351 lines
19 KiB
JavaScript
351 lines
19 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, router } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
Plus,
|
|
Edit2,
|
|
Trash2,
|
|
Check,
|
|
X,
|
|
Info,
|
|
MoreVertical,
|
|
DollarSign,
|
|
Clock,
|
|
Zap,
|
|
Shield,
|
|
Trophy,
|
|
Search
|
|
} from 'lucide-react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
|
|
export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|
const [plans, setPlans] = useState(initialPlans || [
|
|
{ id: 'basic', name: 'Basic Search', price: 99, duration: 'Monthly', features: ['Browse 500+ workers', '10 Shortlists'], status: 'Active', maxContactPeople: 10, enableJobApply: false },
|
|
{ id: 'premium', name: 'Premium Pass', price: 199, duration: 'Monthly', features: ['Unlimited shortlisting', 'Direct messaging'], status: 'Active', maxContactPeople: 50, enableJobApply: true },
|
|
{ id: 'vip', name: 'VIP Concierge', price: 499, duration: 'Monthly', features: ['Dedicated Manager', '30-day replacement'], status: 'Active', maxContactPeople: 100, enableJobApply: true },
|
|
]);
|
|
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingPlan, setEditingPlan] = useState(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
// Form states
|
|
const [name, setName] = useState('');
|
|
const [price, setPrice] = useState('');
|
|
const [duration, setDuration] = useState('Monthly');
|
|
const [features, setFeatures] = useState('');
|
|
const [maxContactPeople, setMaxContactPeople] = useState('');
|
|
const [enableJobApply, setEnableJobApply] = useState(false);
|
|
|
|
const handleCreateClick = () => {
|
|
setEditingPlan(null);
|
|
setName('');
|
|
setPrice('');
|
|
setDuration('Monthly');
|
|
setFeatures('');
|
|
setMaxContactPeople('');
|
|
setEnableJobApply(false);
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleEdit = (plan) => {
|
|
setEditingPlan(plan);
|
|
setName(plan.name);
|
|
setPrice(plan.price);
|
|
setDuration(plan.duration);
|
|
setFeatures(plan.features.join('\n'));
|
|
setMaxContactPeople(plan.maxContactPeople !== undefined ? plan.maxContactPeople : '');
|
|
setEnableJobApply(!!plan.enableJobApply);
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = (id) => {
|
|
if (confirm('Are you sure you want to delete this plan?')) {
|
|
setPlans(plans.filter(p => p.id !== id));
|
|
}
|
|
};
|
|
|
|
const handleSave = (e) => {
|
|
e.preventDefault();
|
|
if (!name.trim() || !price) {
|
|
alert('Please fill out Name and Price.');
|
|
return;
|
|
}
|
|
|
|
const featuresArray = features
|
|
.split('\n')
|
|
.map(f => f.trim())
|
|
.filter(Boolean);
|
|
|
|
if (editingPlan) {
|
|
setPlans(plans.map(p => p.id === editingPlan.id ? {
|
|
...p,
|
|
name,
|
|
price: Number(price),
|
|
duration,
|
|
features: featuresArray,
|
|
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
|
|
enableJobApply
|
|
} : p));
|
|
} else {
|
|
const newId = name.toLowerCase().replace(/\s+/g, '-');
|
|
setPlans([...plans, {
|
|
id: newId || `plan-${Date.now()}`,
|
|
name,
|
|
price: Number(price),
|
|
duration,
|
|
features: featuresArray,
|
|
status: 'Active',
|
|
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
|
|
enableJobApply
|
|
}]);
|
|
}
|
|
setIsDialogOpen(false);
|
|
};
|
|
|
|
const filteredPlans = plans.filter(plan =>
|
|
plan.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
plan.duration.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
plan.features.some(f => f.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
);
|
|
|
|
return (
|
|
<AdminLayout title="Subscription Management">
|
|
<Head title="Plans Management" />
|
|
|
|
<div className="space-y-6">
|
|
{/* Header Actions */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search plans..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleCreateClick}
|
|
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Create New Plan</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Plans Table */}
|
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Plan Name</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Pricing (AED)</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Duration</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Max Contacts</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Job Apply</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Key Features</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Status</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 text-xs h-12 text-right pr-6">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredPlans.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="text-center py-8 text-slate-400 text-sm">
|
|
No plans found
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredPlans.map((plan) => (
|
|
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
|
<TableCell className="py-4">
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
|
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
|
|
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
|
|
'bg-amber-50 text-amber-600'
|
|
}`}>
|
|
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
|
|
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
|
|
<Trophy className="w-4 h-4" />}
|
|
</div>
|
|
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
|
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
|
<TableCell className="text-sm text-slate-700 font-bold">{plan.maxContactPeople || 'Unlimited'}</TableCell>
|
|
<TableCell>
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${
|
|
plan.enableJobApply ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600'
|
|
}`}>
|
|
{plan.enableJobApply ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-wrap gap-1">
|
|
{plan.features.map((f, i) => (
|
|
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
|
|
{f}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
|
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
|
|
}`}>
|
|
{plan.status}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-right pr-6">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
<button
|
|
onClick={() => handleEdit(plan)}
|
|
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(plan.id)}
|
|
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Plan Modal */}
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
|
{editingPlan ? 'Edit Subscription Plan' : 'Create New Plan'}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs font-medium text-slate-500 uppercase tracking-widest">
|
|
Configure pricing and limits for employers
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSave}>
|
|
<div className="grid gap-6 py-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
|
<input
|
|
required
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
placeholder="e.g. Pro Search"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
|
<input
|
|
required
|
|
type="number"
|
|
value={price}
|
|
onChange={(e) => setPrice(e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
placeholder="299"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
|
<select
|
|
value={duration}
|
|
onChange={(e) => setDuration(e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
>
|
|
<option value="Monthly">Monthly</option>
|
|
<option value="Quarterly">Quarterly</option>
|
|
<option value="Yearly">Yearly</option>
|
|
</select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Max Contact People</label>
|
|
<input
|
|
type="number"
|
|
value={maxContactPeople}
|
|
onChange={(e) => setMaxContactPeople(e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
placeholder="e.g. 10 (blank for unlimited)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3 bg-slate-50 p-3.5 rounded-xl border border-slate-100/50">
|
|
<input
|
|
type="checkbox"
|
|
id="enableJobApply"
|
|
checked={enableJobApply}
|
|
onChange={(e) => setEnableJobApply(e.target.checked)}
|
|
className="w-4.5 h-4.5 text-teal-600 border-slate-300 rounded focus:ring-teal-500"
|
|
/>
|
|
<label htmlFor="enableJobApply" className="text-xs font-bold text-slate-700 cursor-pointer select-none">
|
|
Enable Job Apply Module
|
|
<span className="block text-[10px] font-medium text-slate-400 mt-0.5">Allows employers subscribed to this plan to receive job applications.</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
|
<textarea
|
|
value={features}
|
|
onChange={(e) => setFeatures(e.target.value)}
|
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]"
|
|
placeholder="Unlimited search Verified badges"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
|
>
|
|
Save Changes
|
|
</button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</AdminLayout>
|
|
);
|
|
}
|