Yaltopia-Ticket-Admin/src/pages/admin/subscriptions/plans/[id].tsx

251 lines
8.8 KiB
TypeScript

import { useEffect, useState } from "react"
import { useParams, useNavigate } from "react-router-dom"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ArrowLeft, Loader2 } from "lucide-react"
import { subscriptionService, type PlanFeatures } from "@/services"
import { toast } from "sonner"
import type { ApiError } from "@/types/error.types"
function formatFeatureLabel(key: string) {
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
}
export default function PlanManagementPage() {
const { id } = useParams()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [displayName, setDisplayName] = useState("")
const [description, setDescription] = useState("")
const [monthlyPrice, setMonthlyPrice] = useState("")
const [yearlyPrice, setYearlyPrice] = useState("")
const [isActive, setIsActive] = useState(true)
const [features, setFeatures] = useState<PlanFeatures>({ features: {}, limits: {} })
const { data: plan, isLoading } = useQuery({
queryKey: ['admin', 'subscription-plans', id],
queryFn: () => subscriptionService.getAdminPlan(id!),
enabled: !!id,
})
useEffect(() => {
if (plan) {
setDisplayName(plan.displayName)
setDescription(plan.description ?? "")
setMonthlyPrice(String(plan.monthlyPrice))
setYearlyPrice(String(plan.yearlyPrice))
setIsActive(plan.isActive)
setFeatures(plan.features)
}
}, [plan])
const updatePlanMutation = useMutation({
mutationFn: () =>
subscriptionService.updatePlan(id!, {
displayName,
description,
monthlyPrice: Number(monthlyPrice),
yearlyPrice: Number(yearlyPrice),
isActive,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'subscription-plans'] })
toast.success("Plan settings updated")
},
onError: (error) => {
const apiError = error as ApiError
toast.error(apiError.response?.data?.message || "Failed to update plan")
},
})
const updateFeaturesMutation = useMutation({
mutationFn: () => subscriptionService.updatePlanFeatures(id!, features),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'subscription-plans'] })
toast.success("Plan features updated")
},
onError: (error) => {
const apiError = error as ApiError
toast.error(apiError.response?.data?.message || "Failed to update features")
},
})
const toggleFeature = (key: string) => {
setFeatures((prev) => ({
...prev,
features: { ...prev.features, [key]: !prev.features[key] },
}))
}
const updateLimit = (key: string, value: string) => {
const parsed = value.trim() === "" ? null : Number(value)
setFeatures((prev) => ({
...prev,
limits: { ...prev.limits, [key]: parsed },
}))
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-16 text-muted-foreground">
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Loading plan...
</div>
)
}
if (!plan) {
return <div className="text-center py-16">Plan not found.</div>
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate('/admin/subscriptions')}>
<ArrowLeft className="w-4 h-4" />
</Button>
<div>
<h2 className="text-3xl font-bold">{plan.displayName}</h2>
<p className="text-muted-foreground">{plan.name} plan</p>
</div>
</div>
<Tabs defaultValue="pricing">
<TabsList>
<TabsTrigger value="pricing">Pricing & Status</TabsTrigger>
<TabsTrigger value="features">Features & Limits</TabsTrigger>
</TabsList>
<TabsContent value="pricing">
<Card>
<CardHeader>
<CardTitle>Pricing Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4 max-w-lg">
<div className="space-y-2">
<Label htmlFor="displayName">Display Name</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{!plan.isFree && (
<>
<div className="space-y-2">
<Label htmlFor="monthlyPrice">Monthly Price (ETB)</Label>
<Input
id="monthlyPrice"
type="number"
min={0}
value={monthlyPrice}
onChange={(e) => setMonthlyPrice(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="yearlyPrice">Yearly Price (ETB)</Label>
<Input
id="yearlyPrice"
type="number"
min={0}
value={yearlyPrice}
onChange={(e) => setYearlyPrice(e.target.value)}
/>
</div>
</>
)}
<div className="flex items-center justify-between rounded-lg border p-4">
<div>
<Label htmlFor="isActive">Plan Active</Label>
<p className="text-xs text-muted-foreground">
Inactive plans are hidden from new subscriptions.
</p>
</div>
<Switch id="isActive" checked={isActive} onCheckedChange={setIsActive} />
</div>
<Button
onClick={() => updatePlanMutation.mutate()}
disabled={updatePlanMutation.isPending}
>
{updatePlanMutation.isPending && (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
)}
Save Pricing
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="features">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Feature Flags</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Object.entries(features.features).map(([key, enabled]) => (
<div key={key} className="flex items-center justify-between rounded-lg border p-3">
<Label htmlFor={`feature-${key}`}>{formatFeatureLabel(key)}</Label>
<Switch
id={`feature-${key}`}
checked={enabled}
onCheckedChange={() => toggleFeature(key)}
/>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Usage Limits</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Object.entries(features.limits).map(([key, limit]) => (
<div key={key} className="space-y-1">
<Label htmlFor={`limit-${key}`}>{formatFeatureLabel(key)}</Label>
<Input
id={`limit-${key}`}
type="number"
min={0}
placeholder="Unlimited (empty)"
value={limit === null ? "" : String(limit)}
onChange={(e) => updateLimit(key, e.target.value)}
/>
</div>
))}
</CardContent>
</Card>
</div>
<div className="mt-4">
<Button
onClick={() => updateFeaturesMutation.mutate()}
disabled={updateFeaturesMutation.isPending}
>
{updateFeaturesMutation.isPending && (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
)}
Save Features
</Button>
</div>
</TabsContent>
</Tabs>
</div>
)
}