143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { settingsService, type Setting } from "@/services";
|
|
import { toast } from "sonner";
|
|
import type { ApiError } from "@/types/error.types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export default function SettingsPage() {
|
|
const queryClient = useQueryClient();
|
|
const [selectedCategory, setSelectedCategory] = useState<string>("GENERAL");
|
|
|
|
const { data: settings, isLoading } = useQuery({
|
|
queryKey: ["admin", "settings", selectedCategory],
|
|
queryFn: () => settingsService.getSettings(selectedCategory),
|
|
});
|
|
|
|
const updateSettingMutation = useMutation({
|
|
mutationFn: ({ key, value }: { key: string; value: string }) =>
|
|
settingsService.updateSetting(key, { value }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["admin", "settings"] });
|
|
toast.success("Setting updated successfully");
|
|
},
|
|
onError: (error) => {
|
|
const apiError = error as ApiError;
|
|
toast.error(
|
|
apiError.response?.data?.message || "Failed to update setting",
|
|
);
|
|
},
|
|
});
|
|
|
|
const handleSave = (key: string, value: string) => {
|
|
updateSettingMutation.mutate({ key, value });
|
|
};
|
|
|
|
const categories = [
|
|
"GENERAL",
|
|
"EMAIL",
|
|
"STORAGE",
|
|
"SECURITY",
|
|
"API",
|
|
"FEATURES",
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-7xl mx-auto bg-white p-4 min-h-screen">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
|
|
System Settings
|
|
</h1>
|
|
<p className="text-gray-500 mt-1">
|
|
Configure global application parameters.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{/* View only access: Create Setting button removed */}
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs
|
|
value={selectedCategory}
|
|
onValueChange={setSelectedCategory}
|
|
className="space-y-6"
|
|
>
|
|
<TabsList className="bg-gray-100/50 p-1 rounded-none border border-gray-200">
|
|
{categories.map((cat) => (
|
|
<TabsTrigger
|
|
key={cat}
|
|
value={cat}
|
|
className="rounded-none data-[state=active]:bg-white data-[state=active]:shadow-sm px-6 text-[10px] font-bold uppercase tracking-widest transition-all"
|
|
>
|
|
{cat}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
|
|
<TabsContent value={selectedCategory} className="outline-none">
|
|
<Card className="border shadow-none rounded-none">
|
|
<CardHeader className="border-b pb-4 bg-gray-50/30">
|
|
<CardTitle className="text-xs font-bold uppercase tracking-widest text-gray-400">
|
|
{selectedCategory} Configuration
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0 divide-y">
|
|
{isLoading ? (
|
|
<div className="p-20 text-center text-gray-400 animate-pulse font-medium uppercase tracking-widest text-[10px]">
|
|
Fetching system variables...
|
|
</div>
|
|
) : settings && settings.length > 0 ? (
|
|
settings.map((setting: Setting) => (
|
|
<div
|
|
key={setting.key}
|
|
className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-6 hover:bg-gray-50/50 transition-colors"
|
|
>
|
|
<div className="max-w-md">
|
|
<Label
|
|
className="text-sm font-bold text-gray-900 uppercase tracking-tighter"
|
|
htmlFor={setting.key}
|
|
>
|
|
{setting.key.replace(/\./g, " / ")}
|
|
</Label>
|
|
{setting.description && (
|
|
<p className="text-xs text-gray-400 mt-1 leading-relaxed">
|
|
{setting.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 md:max-w-sm">
|
|
<Input
|
|
id={setting.key}
|
|
defaultValue={setting.value}
|
|
className={cn(
|
|
"h-10 rounded-none border-gray-200 text-sm font-medium focus-visible:ring-gray-900",
|
|
updateSettingMutation.isPending &&
|
|
"opacity-50 pointer-events-none",
|
|
)}
|
|
onBlur={(e) => {
|
|
if (e.target.value !== setting.value) {
|
|
handleSave(setting.key, e.target.value);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="p-20 text-center text-gray-400 italic">
|
|
No variables defined for this category.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|