feat: add deals, tasks, and organizations pages with CRUD functionality

- Implemented DealsPage with deal creation, updating, and filtering features.
- Added OrganizationsPage to manage and switch between organizations.
- Created TasksPage for task management, including task creation and filtering.
- Updated router to include new pages for navigation.
This commit is contained in:
Artem Kashaev
2025-12-01 13:46:56 +05:00
parent 4fe3d0480e
commit 8718df9686
21 changed files with 1917 additions and 1 deletions
@@ -0,0 +1,91 @@
import { Building2, RefreshCw } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useToast } from '@/components/ui/use-toast'
import { useOrganizationsQuery, useInvalidateOrganizations } from '@/features/organizations/hooks'
import { useAuthStore } from '@/stores/auth-store'
import { formatDate } from '@/lib/utils'
const OrganizationsPage = () => {
const { data: organizations, isLoading, isFetching } = useOrganizationsQuery()
const activeOrganizationId = useAuthStore((state) => state.activeOrganizationId)
const setActiveOrganization = useAuthStore((state) => state.setActiveOrganization)
const invalidate = useInvalidateOrganizations()
const { toast } = useToast()
const handleSwitch = (id: number) => {
setActiveOrganization(id)
toast({ title: 'Контекст переключён', description: 'Все запросы теперь выполняются в выбранной организации.' })
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-semibold text-foreground">Организации</h1>
<p className="text-sm text-muted-foreground">Список компаний, к которым у вас есть доступ.</p>
</div>
<Button variant="outline" size="sm" className="gap-2" onClick={invalidate} disabled={isFetching}>
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
Обновить
</Button>
</div>
{isLoading ? (
<div className="grid gap-4 lg:grid-cols-2">
{[...Array(2)].map((_, index) => (
<Card key={index} className="border-dashed">
<CardHeader>
<Skeleton className="h-4 w-32" />
</CardHeader>
<CardContent>
<Skeleton className="h-6 w-24" />
<Skeleton className="mt-4 h-4 w-40" />
</CardContent>
</Card>
))}
</div>
) : organizations && organizations.length ? (
<div className="grid gap-4 lg:grid-cols-2">
{organizations.map((org) => (
<Card key={org.id} className={org.id === activeOrganizationId ? 'border-primary shadow-md' : undefined}>
<CardHeader className="flex flex-row items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
<Building2 className="h-5 w-5" />
</div>
<div>
<CardTitle className="text-lg">{org.name}</CardTitle>
<CardDescription>ID {org.id}</CardDescription>
</div>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Создана {formatDate(org.created_at)}</p>
{org.id === activeOrganizationId ? (
<p className="text-sm font-medium text-primary">Активная организация</p>
) : null}
</div>
{org.id === activeOrganizationId ? null : (
<Button variant="outline" size="sm" onClick={() => handleSwitch(org.id)}>
Сделать активной
</Button>
)}
</CardContent>
</Card>
))}
</div>
) : (
<Card className="border-dashed text-center">
<CardHeader>
<CardTitle>Нет организаций</CardTitle>
<CardDescription>Обратитесь к администратору, чтобы вас добавили в рабочую область.</CardDescription>
</CardHeader>
</Card>
)}
</div>
)
}
export default OrganizationsPage