diff --git a/client/src/components/TaskRegistry.tsx b/client/src/components/TaskRegistry.tsx index abe6454..79a1e17 100644 --- a/client/src/components/TaskRegistry.tsx +++ b/client/src/components/TaskRegistry.tsx @@ -618,6 +618,14 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = } }; + const handleRowMouseDown = (e: React.MouseEvent) => { + if (!isAdmin) return; + // Предотвращаем браузерное выделение текста при Ctrl/Cmd/Shift + клике + if (e.ctrlKey || e.metaKey || e.shiftKey) { + e.preventDefault(); + } + }; + const handleRowClick = (task: TaskData, e: React.MouseEvent) => { if (!isAdmin || !onToggleSelected) { onTaskClick?.(task); @@ -896,6 +904,7 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = data-testid={`row-task-${task.id}`} className={`${isCompleted ? 'bg-gray-100 dark:bg-gray-900 opacity-70' : ''} ${isOverdue ? 'border-l-2 border-l-red-500 bg-red-50 dark:bg-red-950/20' : ''} ${isAdmin ? 'cursor-pointer' : ''}`} onClick={(e) => handleRowClick(task, e)} + onMouseDown={handleRowMouseDown} > {isAdmin && ( e.stopPropagation()}> diff --git a/client/src/pages/TaskView.tsx b/client/src/pages/TaskView.tsx index 773b49c..854113b 100644 --- a/client/src/pages/TaskView.tsx +++ b/client/src/pages/TaskView.tsx @@ -76,6 +76,7 @@ const TaskView = () => { const [createTaskDialogOpen, setCreateTaskDialogOpen] = useState(false); const [selectedTaskIds, setSelectedTaskIds] = useState>(new Set()); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteProgress, setDeleteProgress] = useState<{ processed: number; total: number } | null>(null); // Предзаполнение полей из URL (например, ?prefill_field_3=123 для QR-кодов машин) // Флаг ref, чтобы автооткрытие произошло только один раз при первоначальной загрузке страницы. @@ -243,19 +244,43 @@ const TaskView = () => { }); // Массовое удаление задач (только для администратора приложения) + const BULK_DELETE_CHUNK_SIZE = 100; const bulkDeleteMutation = useMutation({ mutationFn: async (taskIds: number[]) => { - const res = await apiRequest('DELETE', '/api/tasks/bulk', { taskIds }); - if (!res.ok) throw new Error('Ошибка при удалении задач'); - return res.json() as Promise<{ success: boolean; deleted: number; total: number }>; + setDeleteProgress({ processed: 0, total: taskIds.length }); + let totalDeleted = 0; + let totalErrors = 0; + const chunks: number[][] = []; + for (let i = 0; i < taskIds.length; i += BULK_DELETE_CHUNK_SIZE) { + chunks.push(taskIds.slice(i, i + BULK_DELETE_CHUNK_SIZE)); + } + let processed = 0; + for (const chunk of chunks) { + const res = await apiRequest('DELETE', '/api/tasks/bulk', { taskIds: chunk }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `Ошибка при удалении пакета задач`); + } + const data = await res.json() as { success: boolean; deleted: number; total: number; errors?: Array<{ taskId: number; error: string }> }; + totalDeleted += data.deleted ?? 0; + totalErrors += data.errors?.length ?? 0; + processed += chunk.length; + setDeleteProgress({ processed, total: taskIds.length }); + } + return { deleted: totalDeleted, total: taskIds.length, errors: totalErrors }; }, onSuccess: (data) => { + setDeleteProgress(null); queryClient.invalidateQueries({ queryKey: ['/api/forms', formId, 'tasks', 'with-fields'] }); setSelectedTaskIds(new Set()); setDeleteDialogOpen(false); - toast({ title: 'Удалено', description: `Удалено задач: ${data.deleted}` }); + const desc = data.errors > 0 + ? `Удалено задач: ${data.deleted}, ошибок: ${data.errors}` + : `Удалено задач: ${data.deleted}`; + toast({ title: 'Удалено', description: desc }); }, onError: (err: any) => { + setDeleteProgress(null); toast({ title: 'Ошибка', description: err.message, variant: 'destructive' }); }, }); @@ -533,6 +558,11 @@ const TaskView = () => { Удалить задачи Будет удалено {selectedTaskIds.size} задач. Это действие нельзя отменить. + {selectedTaskIds.size > BULK_DELETE_CHUNK_SIZE && ( + + Будет разбито на пакеты по {BULK_DELETE_CHUNK_SIZE} задач. + + )}
@@ -552,7 +582,9 @@ const TaskView = () => { disabled={bulkDeleteMutation.isPending} onClick={() => bulkDeleteMutation.mutate(Array.from(selectedTaskIds))} > - {bulkDeleteMutation.isPending ? 'Удаление...' : `Удалить ${selectedTaskIds.size}`} + {bulkDeleteMutation.isPending && deleteProgress + ? `Удаление ${deleteProgress.processed}/${deleteProgress.total}...` + : `Удалить ${selectedTaskIds.size}`}
diff --git a/server/routes/task-bulk.routes.ts b/server/routes/task-bulk.routes.ts index 6a8b1fa..9cad084 100644 --- a/server/routes/task-bulk.routes.ts +++ b/server/routes/task-bulk.routes.ts @@ -9,7 +9,7 @@ import type { ReminderRecipient } from '@shared/schema'; const taskBulkRouter = Router(); -const MAX_BULK_TASKS = 100; +const MAX_BULK_TASKS = 1000; function validateTaskIds(body: unknown): number[] | null { if (!body || typeof body !== 'object') return null;