first commit
This commit is contained in:
461
src/pages/tours/ui/ToursSetting.tsx
Normal file
461
src/pages/tours/ui/ToursSetting.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent } from "@/shared/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { Edit2, Plus, Search, Trash2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface Badge {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Tariff {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface Transport {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface MealPlan {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface HotelType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TabId = "badges" | "tariffs" | "transports" | "mealPlans" | "hotelTypes";
|
||||
|
||||
type DataItem = Badge | Tariff | Transport | MealPlan | HotelType;
|
||||
|
||||
interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: "text" | "number" | "color" | "textarea" | "select";
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
id: TabId;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const ToursSetting: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabId>("badges");
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [modalMode, setModalMode] = useState<"add" | "edit">("add");
|
||||
const [currentItem, setCurrentItem] = useState<DataItem | null>(null);
|
||||
|
||||
const [badges, setBadges] = useState<Badge[]>([
|
||||
{ id: 1, name: "Bestseller", color: "#FFD700" },
|
||||
{ id: 2, name: "Yangi", color: "#4CAF50" },
|
||||
]);
|
||||
|
||||
const [tariffs, setTariffs] = useState<Tariff[]>([
|
||||
{ id: 1, name: "Standart", price: 500 },
|
||||
{ id: 2, name: "Premium", price: 1000 },
|
||||
]);
|
||||
|
||||
const [transports, setTransports] = useState<Transport[]>([
|
||||
{ id: 1, name: "Avtobus", price: 200 },
|
||||
{ id: 2, name: "Minivan", price: 500 },
|
||||
]);
|
||||
|
||||
const [mealPlans, setMealPlans] = useState<MealPlan[]>([
|
||||
{ id: 1, name: "BB (Bed & Breakfast)" },
|
||||
{ id: 2, name: "HB (Half Board)" },
|
||||
{ id: 3, name: "FB (Full Board)" },
|
||||
]);
|
||||
|
||||
const [hotelTypes, setHotelTypes] = useState<HotelType[]>([
|
||||
{ id: 1, name: "3 Yulduz" },
|
||||
{ id: 2, name: "5 Yulduz" },
|
||||
]);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<DataItem>>({});
|
||||
|
||||
const getCurrentData = (): DataItem[] => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return badges;
|
||||
case "tariffs":
|
||||
return tariffs;
|
||||
case "transports":
|
||||
return transports;
|
||||
case "mealPlans":
|
||||
return mealPlans;
|
||||
case "hotelTypes":
|
||||
return hotelTypes;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSetterFunction = (): React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
> => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return setBadges as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "tariffs":
|
||||
return setTariffs as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "transports":
|
||||
return setTransports as React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
>;
|
||||
case "mealPlans":
|
||||
return setMealPlans as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "hotelTypes":
|
||||
return setHotelTypes as React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
>;
|
||||
default:
|
||||
return (() => {}) as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = getCurrentData().filter((item) =>
|
||||
Object.values(item).some((val) =>
|
||||
val?.toString().toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
const getFormFields = (): FormField[] => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return [
|
||||
{ name: "name", label: "Nomi", type: "text", required: true },
|
||||
{ name: "color", label: "Rang", type: "color", required: true },
|
||||
];
|
||||
case "tariffs":
|
||||
return [
|
||||
{ name: "name", label: "Tarif nomi", type: "text", required: true },
|
||||
{ name: "price", label: "Narx", type: "number", required: true },
|
||||
];
|
||||
case "transports":
|
||||
return [
|
||||
{
|
||||
name: "name",
|
||||
label: "Transport nomi",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{ name: "capacity", label: "Sig'im", type: "number", required: true },
|
||||
];
|
||||
case "mealPlans":
|
||||
return [{ name: "name", label: "Nomi", type: "text", required: true }];
|
||||
case "hotelTypes":
|
||||
return [
|
||||
{ name: "name", label: "Tur nomi", type: "text", required: true },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (
|
||||
mode: "add" | "edit",
|
||||
item: DataItem | null = null,
|
||||
): void => {
|
||||
setModalMode(mode);
|
||||
setCurrentItem(item);
|
||||
if (mode === "edit" && item) {
|
||||
setFormData(item);
|
||||
} else {
|
||||
setFormData({});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = (): void => {
|
||||
setIsModalOpen(false);
|
||||
setFormData({});
|
||||
setCurrentItem(null);
|
||||
};
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
const setter = getSetterFunction();
|
||||
|
||||
if (modalMode === "add") {
|
||||
const newId = Math.max(...getCurrentData().map((i) => i.id), 0) + 1;
|
||||
setter([...getCurrentData(), { ...formData, id: newId } as DataItem]);
|
||||
} else {
|
||||
setter(
|
||||
getCurrentData().map((item) =>
|
||||
item.id === currentItem?.id ? { ...item, ...formData } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const handleDelete = (id: number): void => {
|
||||
if (window.confirm("Rostdan ham o'chirmoqchimisiz?")) {
|
||||
const setter = getSetterFunction();
|
||||
setter(getCurrentData().filter((item) => item.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: "badges", label: "Belgilar" },
|
||||
{ id: "tariffs", label: "Tariflar" },
|
||||
{ id: "transports", label: "Transportlar" },
|
||||
{ id: "mealPlans", label: "Ovqatlanish" },
|
||||
{ id: "hotelTypes", label: "Otel turlari" },
|
||||
];
|
||||
|
||||
const getFieldValue = (fieldName: string): string | number => {
|
||||
return (formData as Record<string, string | number>)[fieldName] || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-6 w-full">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Tur Sozlamalari</h1>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => {
|
||||
setActiveTab(v as TabId);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<Card className="bg-gray-900">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground"
|
||||
size={20}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Qidirish..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => openModal("add")}
|
||||
className="w-full sm:w-auto cursor-pointer"
|
||||
>
|
||||
<Plus size={20} className="mr-2" />
|
||||
Yangi qo'shish
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-full">
|
||||
<div className="border-b">
|
||||
<div className="flex">
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
|
||||
ID
|
||||
</div>
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
|
||||
Nomi
|
||||
</div>
|
||||
{activeTab === "badges" && (
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-48">
|
||||
Rang
|
||||
</div>
|
||||
)}
|
||||
{(activeTab === "tariffs" || activeTab === "transports") && (
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Narx
|
||||
</div>
|
||||
)}
|
||||
<div className="px-6 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Amallar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{filteredData.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center text-muted-foreground">
|
||||
Ma'lumot topilmadi
|
||||
</div>
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="px-6 py-4 w-20">{item.id}</div>
|
||||
<div className="px-6 py-4 font-medium flex-1">
|
||||
{item.name}
|
||||
</div>
|
||||
{activeTab === "badges" && (
|
||||
<div className="px-6 py-4 w-48">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded border"
|
||||
style={{ backgroundColor: (item as Badge).color }}
|
||||
/>
|
||||
<span>{(item as Badge).color}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(activeTab === "tariffs" ||
|
||||
activeTab === "transports") && (
|
||||
<div className="px-6 py-4 w-32">
|
||||
{(item as Tariff).price}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-6 py-4 w-32">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => openModal("edit", item)}
|
||||
>
|
||||
<Edit2 size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} className="text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-gray-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{modalMode === "add" ? "Yangi qo'shish" : "Tahrirlash"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{getFormFields().map((field) => (
|
||||
<div key={field.name} className="space-y-2">
|
||||
<Label htmlFor={field.name}>
|
||||
{field.label}
|
||||
{field.required && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</Label>
|
||||
{field.type === "textarea" ? (
|
||||
<Textarea
|
||||
id={field.name}
|
||||
value={getFieldValue(field.name) as string}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[field.name]: e.target.value,
|
||||
})
|
||||
}
|
||||
required={field.required}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "select" ? (
|
||||
<Select
|
||||
value={getFieldValue(field.name) as string}
|
||||
onValueChange={(value) =>
|
||||
setFormData({ ...formData, [field.name]: value })
|
||||
}
|
||||
required={field.required}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{opt}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
value={getFieldValue(field.name)}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[field.name]:
|
||||
field.type === "number"
|
||||
? Number(e.target.value)
|
||||
: e.target.value,
|
||||
})
|
||||
}
|
||||
required={field.required}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
className="flex-1"
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit} className="flex-1">
|
||||
Saqlash
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToursSetting;
|
||||
Reference in New Issue
Block a user