Files
iistwin/client/src/pages/Tasks.tsx

236 lines
9.2 KiB
TypeScript
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.

import { useState } from 'react';
import { useLocation, Link } from 'wouter';
import { useQuery } from '@tanstack/react-query';
import Layout from '@/components/Layout';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Plus, ChevronRight, FileText, CheckSquare, ArrowRight } from 'lucide-react';
import { ListSkeleton, Skeleton } from '@/components/ui/skeleton';
import { StaggerList, StaggerItem } from '@/components/transitions/StaggerList';
interface TaskItem {
id: number;
formId: number;
title: string;
currentStatusId: number;
assignedTo?: number;
updatedAt: string;
createdAt: string;
status?: string;
}
interface FormItem {
id: number;
name: string;
description?: string;
isActive: boolean;
}
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'только что';
if (mins < 60) return `${mins} мин назад`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs} ч назад`;
const days = Math.floor(hrs / 24);
if (days < 30) return `${days} д назад`;
return new Date(dateStr).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
}
const Tasks = () => {
const [, setLocation] = useLocation();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const { data: tasksData, isLoading: tasksLoading } = useQuery<{ success: boolean; tasks: TaskItem[] }>({
queryKey: ['/api/tasks?minimal=true&limit=100'],
});
const { data: formsData, isLoading: formsLoading } = useQuery<{ success: boolean; forms: FormItem[] }>({
queryKey: ['/api/forms'],
});
const tasks = tasksData?.tasks || [];
const forms = formsData?.forms || [];
const activeForms = forms.filter(f => f.isActive);
const formMap = new Map(forms.map(f => [f.id, f]));
const isLoading = tasksLoading || formsLoading;
return (
<Layout>
<div className="p-3 space-y-3">
<div className="flex items-center justify-between">
<div>
<h1 className="text-sm font-medium text-foreground">Задачи</h1>
<div className="text-[10px] text-muted-foreground">
{isLoading ? <Skeleton className="h-3 w-24" /> : `${tasks.length} задач в системе`}
</div>
</div>
<Button
className="h-7 px-2.5 text-xs"
onClick={() => setCreateDialogOpen(true)}
disabled={formsLoading || activeForms.length === 0}
data-testid="button-create-task"
>
<Plus className="w-3.5 h-3.5 mr-1" />
Создать
</Button>
</div>
{isLoading ? (
<ListSkeleton rows={8} />
) : tasks.length === 0 ? (
<EmptyState forms={activeForms} onCreateClick={() => setCreateDialogOpen(true)} />
) : (
<StaggerList className="border border-border rounded divide-y divide-border overflow-hidden">
{tasks.map(task => {
const form = formMap.get(task.formId);
return (
<StaggerItem
key={task.id}
className="flex items-center gap-2 p-2.5 hover:bg-muted/40 cursor-pointer transition-colors"
onClick={() => setLocation(`/forms/${task.formId}/tasks/${task.id}`)}
data-testid={`task-row-${task.id}`}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5 flex-wrap">
{form && (
<span className="text-[10px] text-muted-foreground font-medium shrink-0">
{form.name}
</span>
)}
{task.status && (
<Badge variant="outline" className="text-[10px] h-4 px-1 py-0 font-normal shrink-0">
{task.status}
</Badge>
)}
</div>
<p className="text-xs truncate">{task.title}</p>
</div>
<div className="text-[10px] text-muted-foreground shrink-0 hidden sm:block">
{timeAgo(task.updatedAt)}
</div>
<ChevronRight className="w-3 h-3 text-muted-foreground shrink-0" />
</StaggerItem>
);
})}
</StaggerList>
)}
</div>
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="text-sm">Выберите форму</DialogTitle>
<DialogDescription className="text-xs">
Задача будет создана в выбранной форме
</DialogDescription>
</DialogHeader>
<div className="space-y-1 max-h-72 overflow-y-auto">
{activeForms.length === 0 ? (
<div className="text-center py-4 space-y-2">
<p className="text-xs text-muted-foreground">Нет доступных форм</p>
<Link href="/forms">
<Button variant="outline" className="h-7 px-2.5 text-xs">
Создать форму
<ArrowRight className="w-3 h-3 ml-1" />
</Button>
</Link>
</div>
) : (
activeForms.map(form => (
<button
key={form.id}
className="w-full flex items-center gap-2 p-2 rounded text-left hover:bg-muted transition-colors"
onClick={() => {
setCreateDialogOpen(false);
setLocation(`/forms/${form.id}/tasks`);
}}
data-testid={`form-option-${form.id}`}
>
<FileText className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs">{form.name}</p>
{form.description && (
<p className="text-[10px] text-muted-foreground truncate">{form.description}</p>
)}
</div>
<ChevronRight className="w-3 h-3 text-muted-foreground" />
</button>
))
)}
</div>
</DialogContent>
</Dialog>
</Layout>
);
};
function EmptyState({ forms, onCreateClick }: { forms: FormItem[]; onCreateClick: () => void }) {
return (
<div className="space-y-3">
<div className="border border-border rounded p-6 flex flex-col items-center gap-2 text-center">
<CheckSquare className="w-8 h-8 text-muted-foreground/40" />
{forms.length === 0 ? (
<>
<div>
<p className="text-xs font-medium">Пока нет ни одной формы</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
Задачи создаются внутри форм — начните с создания первой формы
</p>
</div>
<Link href="/forms">
<Button className="h-7 px-2.5 text-xs mt-1">
<Plus className="w-3.5 h-3.5 mr-1" />
Создать форму
<ArrowRight className="w-3 h-3 ml-1" />
</Button>
</Link>
</>
) : (
<>
<div>
<p className="text-xs font-medium">Задач пока нет</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
Выберите форму и создайте первую задачу
</p>
</div>
<Button className="h-7 px-2.5 text-xs mt-1" onClick={onCreateClick}>
<Plus className="w-3.5 h-3.5 mr-1" />
Создать задачу
</Button>
</>
)}
</div>
{forms.length > 0 && (
<div>
<p className="text-[10px] text-muted-foreground font-medium mb-1.5 px-0.5">
Доступные формы
</p>
<div className="border border-border rounded divide-y divide-border overflow-hidden">
{forms.map(form => (
<Link key={form.id} href={`/forms/${form.id}/tasks`}>
<div className="flex items-center gap-2 p-2.5 hover:bg-muted/40 cursor-pointer transition-colors">
<FileText className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs font-medium">{form.name}</p>
{form.description && (
<p className="text-[10px] text-muted-foreground truncate">{form.description}</p>
)}
</div>
<ChevronRight className="w-3 h-3 text-muted-foreground" />
</div>
</Link>
))}
</div>
</div>
)}
</div>
);
}
export default Tasks;