Услуги

This commit is contained in:
Халимов Рустам
2026-03-05 15:24:49 +03:00
parent b651f53958
commit 663ec55887
275 changed files with 6775 additions and 907 deletions
+142
View File
@@ -0,0 +1,142 @@
"use client";
import { useEffect, useState } from "react";
import { getMyOffers, Offer } from "@/shared/api/catalog";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { PlusCircle, Loader2, Pencil, Trash2, PauseCircle, PlayCircle } from "lucide-react";
import { useSessionStore } from "@/entities/session/store";
export default function MyOffersPage() {
const router = useRouter();
const { user, isAuth } = useSessionStore();
const [offers, setOffers] = useState<Offer[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Проверка прав доступа
if (!isAuth) return;
const isPerformer = user?.roles?.some(r => ["Master", "Company", "Candidate", "Admin"].includes(r));
if (!isPerformer) {
toast.error("Чтобы создавать услуги, вам нужно стать исполнителем");
router.push("/dashboard/become-performer");
return;
}
const fetchOffers = async () => {
try {
const data = await getMyOffers();
setOffers(data || []);
} catch (error) {
console.error(error);
toast.error("Не удалось загрузить ваши услуги");
} finally {
setLoading(false);
}
};
fetchOffers();
}, [isAuth, user, router]);
if (loading) {
return (
<div className="flex justify-center items-center h-48">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (offers.length === 0) {
return (
<div className="container max-w-5xl py-10">
<div className="flex flex-col items-center justify-center p-12 text-center bg-card rounded-2xl border shadow-sm">
<div className="mb-4 rounded-full bg-primary/10 p-4">
<PlusCircle className="h-12 w-12 text-primary" />
</div>
<h2 className="text-2xl font-bold mb-3">У вас еще нет услуг</h2>
<p className="text-muted-foreground mb-8 max-w-md text-lg">
Создайте свою первую услугу, чтобы клиенты могли вас найти и предложить заказ.
</p>
<Link href="/dashboard/offers/create">
<Button size="lg" className="gap-2">
<PlusCircle className="h-5 w-5" />
Создать первую услугу
</Button>
</Link>
</div>
</div>
);
}
return (
<div className="container max-w-5xl py-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 bg-card p-6 rounded-2xl shadow-sm border">
<h1 className="text-3xl font-bold">Мои услуги</h1>
<Link href="/dashboard/offers/create">
<Button className="gap-2">
<PlusCircle className="h-4 w-4" />
Создать услугу
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{offers.map((offer) => (
<Card key={offer.id} className={`flex flex-col transition-all hover:shadow-md ${!offer.isActive ? "bg-muted/30 border-dashed" : "border-border"}`}>
<CardHeader className="pb-3 relative">
<CardTitle className={`text-xl font-semibold line-clamp-1 ${!offer.isActive && "text-muted-foreground"}`}>{offer.title}</CardTitle>
<CardDescription>
{offer.isActive ? (
<span className="inline-flex items-center text-green-600 dark:text-green-500 text-xs font-medium bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-green-600 dark:bg-green-500 mr-1.5"></span>
Активна
</span>
) : (
<span className="inline-flex items-center text-muted-foreground text-xs font-medium bg-muted px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground mr-1.5"></span>
Приостановлена
</span>
)}
</CardDescription>
</CardHeader>
<CardContent className={`flex-1 ${!offer.isActive && "opacity-70 grayscale"}`}>
<p className="text-sm text-foreground/80 line-clamp-3 mb-4 leading-relaxed">
{offer.description}
</p>
<div className="font-semibold text-lg flex items-center gap-1">
{offer.price.amount} {offer.price.currency}
<span className="text-sm font-normal text-muted-foreground relative top-0.5">
{offer.price.type === 0 ? "фиксированно" : offer.price.type === 1 ? "в час" : "договорная"}
</span>
</div>
</CardContent>
<CardFooter className="flex gap-2 justify-end bg-muted/20 pt-4 mt-auto border-t">
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-primary/10 border-transparent hover:border-primary/20 text-muted-foreground hover:text-primary transition-all duration-300 ease-out">
<Pencil className="h-4 w-4 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[120px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
Редактировать
</span>
</Button>
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-orange-500/10 border-transparent hover:border-orange-500/20 text-muted-foreground hover:text-orange-500 transition-all duration-300 ease-out" onClick={() => toast.info("Функция паузы в разработке")}>
{offer.isActive ? <PauseCircle className="h-4 w-4 shrink-0" /> : <PlayCircle className="h-4 w-4 shrink-0" />}
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[120px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
{offer.isActive ? "Приостановить" : "Активировать"}
</span>
</Button>
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-destructive/10 border-transparent hover:border-destructive/20 text-muted-foreground hover:text-destructive transition-all duration-300 ease-out" onClick={() => toast.info("Функция удаления в разработке")}>
<Trash2 className="h-4 w-4 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[100px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
Удалить
</span>
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}