feat(qr): offline show_list диалог для QR-автоматизации КИПиА

- clientAutomationEngine: onShowTaskDialog callback для show_list
- QrHandler: Dialog со списком найденных задач, навигация на /forms/11/tasks/:id
This commit is contained in:
2026-07-11 21:48:35 +03:00
parent af8920f920
commit 833fa97f56
2 changed files with 59 additions and 6 deletions

View File

@@ -208,6 +208,7 @@ export async function handleAutomationActions(
deps: { deps: {
toast: (opts: { title?: string; description?: string; variant?: 'default' | 'destructive' }) => void; toast: (opts: { title?: string; description?: string; variant?: 'default' | 'destructive' }) => void;
navigate?: (path: string) => void; navigate?: (path: string) => void;
onShowTaskDialog?: (tasks: Array<{ id: number; title: string }>) => void;
}, },
): Promise<boolean> { ): Promise<boolean> {
const holder = extractActions(result); const holder = extractActions(result);
@@ -244,7 +245,11 @@ export async function handleAutomationActions(
if (holder.legacy) { if (holder.legacy) {
handled = true; handled = true;
await handleLegacyAction(holder.legacy, deps); await handleLegacyAction(holder.legacy, {
toast: deps.toast,
navigate: deps.navigate,
onShowTaskDialog: deps.onShowTaskDialog,
});
} }
return handled; return handled;
@@ -291,6 +296,7 @@ async function handleLegacyAction(
deps: { deps: {
toast: (opts: { title?: string; description?: string; variant?: 'default' | 'destructive' }) => void; toast: (opts: { title?: string; description?: string; variant?: 'default' | 'destructive' }) => void;
navigate?: (path: string) => void; navigate?: (path: string) => void;
onShowTaskDialog?: (tasks: Array<{ id: number; title: string }>) => void;
}, },
): Promise<void> { ): Promise<void> {
const nav = deps.navigate ?? navigate; const nav = deps.navigate ?? navigate;
@@ -317,9 +323,13 @@ async function handleLegacyAction(
break; break;
} }
case 'show_list': { case 'show_list': {
// Legacy show_list has no direct screen — notify the user instead. if (deps.onShowTaskDialog && action.tasks.length > 0) {
const titles = action.tasks.map((t) => t.title).join(', '); deps.onShowTaskDialog(action.tasks);
deps.toast({ title: 'Найдены задачи', description: titles || 'Список пуст' }); } else {
// Fallback when no dialog handler is provided.
const titles = action.tasks.map((t) => t.title).join(', ');
deps.toast({ title: 'Найдены задачи', description: titles || 'Список пуст' });
}
break; break;
} }
} }

View File

@@ -6,6 +6,13 @@ import { runOfflineAutomations, handleAutomationActions, type ClientAutomationRe
import { checkServerReachable } from '@/lib/syncEngine'; import { checkServerReachable } from '@/lib/syncEngine';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Loader2, Terminal, ArrowLeft } from 'lucide-react'; import { Loader2, Terminal, ArrowLeft } from 'lucide-react';
import { apiRequest } from '@/lib/queryClient'; import { apiRequest } from '@/lib/queryClient';
@@ -17,6 +24,10 @@ export default function QrHandler() {
const [results, setResults] = useState<ClientAutomationResult[]>([]); const [results, setResults] = useState<ClientAutomationResult[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [taskDialog, setTaskDialog] = useState<{ open: boolean; tasks: Array<{ id: number; title: string }> }>({
open: false,
tasks: [],
});
const templateKey = params.templateKey ?? ''; const templateKey = params.templateKey ?? '';
const objectId = params.objectId ? parseInt(params.objectId) : undefined; const objectId = params.objectId ? parseInt(params.objectId) : undefined;
@@ -57,7 +68,11 @@ export default function QrHandler() {
if (res.error) { if (res.error) {
toast({ title: 'Ошибка автоматизации', description: res.error, variant: 'destructive' }); toast({ title: 'Ошибка автоматизации', description: res.error, variant: 'destructive' });
} else if (res.result) { } else if (res.result) {
await handleAutomationActions(res.result, { toast, navigate }); await handleAutomationActions(res.result, {
toast,
navigate,
onShowTaskDialog: (tasks) => setTaskDialog({ open: true, tasks }),
});
} }
} }
} }
@@ -82,7 +97,11 @@ export default function QrHandler() {
); );
for (const res of data.results) { for (const res of data.results) {
if (res.result) { if (res.result) {
await handleAutomationActions(res.result, { toast, navigate }); await handleAutomationActions(res.result, {
toast,
navigate,
onShowTaskDialog: (tasks) => setTaskDialog({ open: true, tasks }),
});
} }
} }
} else { } else {
@@ -165,6 +184,30 @@ export default function QrHandler() {
))} ))}
</CardContent> </CardContent>
</Card> </Card>
<Dialog open={taskDialog.open} onOpenChange={(open) => setTaskDialog((prev) => ({ ...prev, open }))}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="text-base">Найдены заявки</DialogTitle>
<DialogDescription>Выберите задачу для открытия</DialogDescription>
</DialogHeader>
<div className="space-y-2 max-h-[60vh] overflow-auto">
{taskDialog.tasks.map((task) => (
<Button
key={task.id}
variant="outline"
className="w-full justify-start h-auto py-2 px-3 text-left"
onClick={() => {
setTaskDialog({ open: false, tasks: [] });
navigate(`/forms/11/tasks/${task.id}`);
}}
>
<span className="text-sm">#{task.id} {task.title}</span>
</Button>
))}
</div>
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
); );