251 lines
8.8 KiB
TypeScript
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>
|
|
)
|
|
}
|