176 lines
10 KiB
TypeScript
176 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { getMyOffers, Offer, toggleOffer, deleteOffer } 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, Image as ImageIcon } from "lucide-react";
|
||
import { useSessionStore } from "@/entities/session/store";
|
||
import { formatPrice } from "@/shared/lib/formatPrice";
|
||
|
||
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]);
|
||
|
||
const handleToggle = async (id: string) => {
|
||
try {
|
||
const newStatus = await toggleOffer(id);
|
||
setOffers(offers.map(o => o.id === id ? { ...o, isActive: newStatus } : o));
|
||
toast.success(newStatus ? "Услуга активирована" : "Услуга приостановлена");
|
||
} catch {
|
||
toast.error("Не удалось изменить статус услуги");
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: string) => {
|
||
if (!confirm("Вы уверены, что хотите удалить эту услугу?")) return;
|
||
try {
|
||
await deleteOffer(id);
|
||
setOffers(offers.filter(o => o.id !== id));
|
||
toast.success("Услуга удалена");
|
||
} catch {
|
||
toast.error("Не удалось удалить услугу");
|
||
}
|
||
};
|
||
|
||
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"}`}>
|
||
{offer.images && offer.images.length > 0 && (
|
||
<div className="w-full h-32 mb-4 rounded-md bg-muted overflow-hidden relative">
|
||
<img src={offer.images[0]} className="w-full h-full object-cover" alt="Primary image" />
|
||
</div>
|
||
)}
|
||
<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.5">
|
||
{offer.price.type === 2
|
||
? <span className="text-muted-foreground text-base">Договорная</span>
|
||
: <>
|
||
<span>{formatPrice(offer.price.amount)}</span>
|
||
<span className="text-sm font-normal text-muted-foreground relative top-0.5">
|
||
{offer.price.type === 1 ? "в час" : "фиксированно"}
|
||
</span>
|
||
</>}
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter className="flex gap-2 justify-end bg-muted/20 pt-4 mt-auto border-t">
|
||
<Link href={`/dashboard/offers/${offer.id}/edit`}>
|
||
<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>
|
||
</Link>
|
||
<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={() => handleToggle(offer.id)}>
|
||
{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={() => handleDelete(offer.id)}>
|
||
<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>
|
||
);
|
||
}
|