CaiWuXiTong/app/Http/Controllers/Business/SystemizationTodos.php

336 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Business;
use App\Business\SystemizationTodoWorkbookImporter;
use App\Models\Business\SystemizationTodo;
use App\Models\Business\SystemizationTodoAttachment;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Throwable;
class SystemizationTodos extends Controller
{
public function page(): array
{
$filters = $this->filters();
if (
$filters['date_from'] !== ''
&& $filters['date_to'] !== ''
&& $filters['date_from'] > $filters['date_to']
) {
[$filters['date_from'], $filters['date_to']] = [
$filters['date_to'],
$filters['date_from'],
];
}
$query = SystemizationTodo::query()
->with('attachments')
->withCount('attachments');
if ($filters['status'] !== '') {
$query->where('status', $filters['status']);
}
if ($filters['category'] !== '') {
$query->where('category', $filters['category']);
}
if ($filters['owner'] !== '') {
$query->where(function ($builder) use ($filters): void {
$builder->where('owner', $filters['owner'])
->orWhere('assignee', $filters['owner']);
});
}
if ($filters['date_from'] !== '') {
$query->whereDate($filters['date_scope'], '>=', $filters['date_from']);
}
if ($filters['date_to'] !== '') {
$query->whereDate($filters['date_scope'], '<=', $filters['date_to']);
}
if ($filters['keyword'] !== '') {
$like = '%' . addcslashes($filters['keyword'], '%_\\') . '%';
$query->where(function ($builder) use ($like): void {
$builder->where('title', 'like', $like)
->orWhere('category', 'like', $like)
->orWhere('section', 'like', $like)
->orWhere('owner', 'like', $like)
->orWhere('assignee', 'like', $like)
->orWhere('progress', 'like', $like)
->orWhere('notes', 'like', $like);
});
}
$items = $query
->orderByRaw("CASE WHEN status = '已完成' THEN 1 ELSE 0 END")
->orderByRaw("CASE WHEN category IS NULL OR category = '' THEN 1 ELSE 0 END")
->orderBy('category')
->orderByRaw("CASE WHEN section IS NULL OR section = '' THEN 1 ELSE 0 END")
->orderBy('section')
->orderByRaw('CASE WHEN due_date IS NULL THEN 1 ELSE 0 END')
->orderBy('due_date')
->orderBy('sort_order')
->orderBy('id')
->paginate(50)
->withQueryString();
$base = SystemizationTodo::query();
$categories = SystemizationTodo::query()
->whereNotNull('category')
->where('category', '<>', '')
->distinct()
->orderBy('category')
->pluck('category');
$owners = SystemizationTodo::query()
->whereNotNull('owner')
->where('owner', '<>', '')
->pluck('owner')
->merge(
SystemizationTodo::query()
->whereNotNull('assignee')
->where('assignee', '<>', '')
->pluck('assignee')
)
->flatMap(static fn (string $value): array => array_map('trim', preg_split('/[、,,]/u', $value) ?: []))
->filter()
->unique()
->sort()
->values();
return [
'view' => 'business.pages.systemization-todos',
'title' => '系统化待办',
'items' => $items,
'todoFilters' => $filters,
'todoCategories' => $categories,
'todoOwners' => $owners,
'todoStatuses' => SystemizationTodo::STATUSES,
'todoDateScopes' => $this->dateScopes(),
'todoSummary' => [
'total' => (clone $base)->count(),
'pending' => (clone $base)->where('status', SystemizationTodo::STATUS_PENDING)->count(),
'in_progress' => (clone $base)->where('status', SystemizationTodo::STATUS_IN_PROGRESS)->count(),
'completed' => (clone $base)->where('status', SystemizationTodo::STATUS_COMPLETED)->count(),
'overdue' => (clone $base)
->whereNotIn('status', [SystemizationTodo::STATUS_COMPLETED])
->whereNotNull('due_date')
->whereDate('due_date', '<', today())
->count(),
],
'todoRoutes' => [
'store' => route('business.systemization-todos.store'),
'update' => route('business.systemization-todos.update', ['todo' => '__TODO__']),
'destroy' => route('business.systemization-todos.destroy', ['todo' => '__TODO__']),
'attachment' => route('business.systemization-todos.attachments.show', [
'todo' => '__TODO__',
'attachment' => '__ATTACHMENT__',
]),
'import' => route('business.systemization-todos.import'),
],
];
}
public function store(Request $request): RedirectResponse
{
$data = $this->validated($request);
$data['company_id'] = (int) company_id();
$data['sort_order'] = (int) SystemizationTodo::query()->max('sort_order') + 1;
$data['created_by'] = user_id();
$data['updated_by'] = user_id();
$this->normalizeCompletionDates($data);
$todo = SystemizationTodo::query()->create($data);
$this->storeAttachments($request, $todo);
return $this->backToPage('待办事项已新增。');
}
public function update(Request $request, SystemizationTodo $todo): RedirectResponse
{
$data = $this->validated($request);
$data['updated_by'] = user_id();
$this->normalizeCompletionDates($data);
$removeIds = collect($request->input('remove_attachments', []))
->map(static fn (mixed $id): int => (int) $id)
->filter()
->unique()
->values();
if ($removeIds->isNotEmpty()) {
$attachments = $todo->attachments()
->where('company_id', (int) company_id())
->whereIn('id', $removeIds)
->get();
foreach ($attachments as $attachment) {
Storage::disk('uploads')->delete($attachment->path);
$attachment->delete();
}
}
$todo->update($data);
$this->storeAttachments($request, $todo);
return $this->backToPage('待办事项已更新。');
}
public function destroy(SystemizationTodo $todo): RedirectResponse
{
$todo->update(['updated_by' => user_id()]);
$todo->delete();
return $this->backToPage('待办事项已删除。');
}
public function attachment(SystemizationTodo $todo, SystemizationTodoAttachment $attachment): StreamedResponse
{
abort_unless((int) $attachment->systemization_todo_id === (int) $todo->id, 404);
abort_unless((int) $attachment->company_id === (int) company_id(), 404);
$disk = Storage::disk('uploads');
abort_unless($disk->exists($attachment->path), 404, '截图不存在。');
return $disk->response($attachment->path, $attachment->original_name, [
'Cache-Control' => 'private, no-store',
'Content-Type' => $attachment->mime_type,
'X-Content-Type-Options' => 'nosniff',
]);
}
public function importWorkbook(
Request $request,
SystemizationTodoWorkbookImporter $importer,
): RedirectResponse {
$data = $request->validate([
'source' => ['required', 'file', 'mimes:xlsx', 'max:20480'],
]);
try {
$result = $importer->import(
$data['source']->getRealPath(),
(int) company_id(),
user_id(),
);
} catch (Throwable $exception) {
report($exception);
return $this->backToPage('导入失败,请确认工作簿表头、文件格式和内容后重试。', 'error');
}
return $this->backToPage(sprintf(
'导入完成:%s,新增 %d 条,跳过已存在 %d 条。',
$result['sheet'],
$result['created'],
$result['skipped'],
));
}
private function filters(): array
{
$dateScopes = $this->dateScopes();
$dateScope = (string) request('date_scope', 'due_date');
return [
'status' => in_array(request('status'), SystemizationTodo::STATUSES, true)
? (string) request('status')
: '',
'category' => trim((string) request('category', '')),
'owner' => trim((string) request('owner', '')),
'keyword' => trim((string) request('keyword', '')),
'date_scope' => array_key_exists($dateScope, $dateScopes) ? $dateScope : 'due_date',
'date_from' => $this->normalizeDateFilter(request('date_from')),
'date_to' => $this->normalizeDateFilter(request('date_to')),
];
}
private function dateScopes(): array
{
return [
'due_date' => '完成时间',
'start_date' => '开始时间',
'created_at' => '新增时间',
];
}
private function normalizeDateFilter(mixed $value): string
{
$value = trim((string) $value);
if ($value === '') {
return '';
}
$date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value);
return $date instanceof \DateTimeImmutable && $date->format('Y-m-d') === $value
? $value
: '';
}
private function validated(Request $request): array
{
return $request->validate([
'category' => ['nullable', 'string', 'max:100'],
'section' => ['nullable', 'string', 'max:255'],
'title' => ['required', 'string', 'max:5000'],
'owner' => ['nullable', 'string', 'max:255'],
'assignee' => ['nullable', 'string', 'max:255'],
'start_date' => ['nullable', 'date_format:Y-m-d'],
'due_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'],
'status' => ['required', Rule::in(SystemizationTodo::STATUSES)],
'completed_at' => ['nullable', 'date_format:Y-m-d'],
'progress' => ['nullable', 'string', 'max:10000'],
'notes' => ['nullable', 'string', 'max:10000'],
'screenshots' => ['nullable', 'array', 'max:12'],
'screenshots.*' => ['file', 'mimes:jpg,jpeg,png,webp', 'max:10240'],
'remove_attachments' => ['nullable', 'array'],
'remove_attachments.*' => ['integer'],
]);
}
private function normalizeCompletionDates(array &$data): void
{
$completed = ($data['status'] ?? '') === SystemizationTodo::STATUS_COMPLETED;
$data['completed_at'] = $completed
? (($data['completed_at'] ?? null) ?: ($data['due_date'] ?? null) ?: now()->toDateString())
: null;
}
private function storeAttachments(Request $request, SystemizationTodo $todo): void
{
foreach ($request->file('screenshots', []) as $file) {
$extension = strtolower($file->getClientOriginalExtension());
$fileName = (string) Str::uuid() . ($extension !== '' ? '.' . $extension : '');
$path = Storage::disk('uploads')->putFileAs(
'systemization-todos/' . (int) company_id() . '/' . $todo->id,
$file,
$fileName,
);
if ($path === false) {
throw new \RuntimeException('完成截图保存失败。');
}
SystemizationTodoAttachment::query()->create([
'company_id' => (int) company_id(),
'systemization_todo_id' => $todo->id,
'path' => $path,
'original_name' => mb_substr(basename($file->getClientOriginalName()), 0, 240),
'mime_type' => $file->getMimeType() ?: 'application/octet-stream',
'size' => $file->getSize() ?: 0,
'uploaded_by' => user_id(),
]);
}
}
private function backToPage(string $message, string $type = 'success'): RedirectResponse
{
return redirect()
->route('business.pages.show', [
'section' => 'other',
'page' => 'systemization-todos',
])
->with($type === 'success' ? 'success' : 'error', $message);
}
}