458 lines
26 KiB
JavaScript

import React, { useState, useEffect } 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, errors = {} }) {
const mapIncomingPlans = (rawPlans) => {
if (!rawPlans) return [];
return rawPlans.map(p => ({
...p,
price: Number(p.price),
maxContactPeople: p.max_contact_people !== undefined ? p.max_contact_people : p.maxContactPeople,
unlimitedContacts: p.unlimited_contacts !== undefined ? !!p.unlimited_contacts : !!p.unlimitedContacts,
enableJobApply: p.enable_job_apply !== undefined ? !!p.enable_job_apply : !!p.enableJobApply,
usageCount: p.usage_count !== undefined ? Number(p.usage_count) : 0
}));
};
const [plans, setPlans] = useState(() => mapIncomingPlans(initialPlans || []));
useEffect(() => {
if (initialPlans) {
setPlans(mapIncomingPlans(initialPlans));
}
}, [initialPlans]);
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 [unlimitedContacts, setUnlimitedContacts] = useState(false);
const [enableJobApply, setEnableJobApply] = useState(false);
const [status, setStatus] = useState('Active');
const handleCreateClick = () => {
setEditingPlan(null);
setName('');
setPrice('');
setDuration('Monthly');
setFeatures('');
setMaxContactPeople('');
setUnlimitedContacts(false);
setEnableJobApply(false);
setStatus('Active');
setIsDialogOpen(true);
};
const handleEdit = (plan) => {
setEditingPlan(plan);
setName(plan.name);
setPrice(plan.price);
setDuration(plan.duration);
setFeatures(plan.features.join('\n'));
setMaxContactPeople(plan.maxContactPeople !== null && plan.maxContactPeople !== undefined ? plan.maxContactPeople : '');
setUnlimitedContacts(!!plan.unlimitedContacts);
setEnableJobApply(!!plan.enableJobApply);
setStatus(plan.status || 'Active');
setIsDialogOpen(true);
};
const handleDelete = (id) => {
if (confirm('Are you sure you want to delete this plan?')) {
router.delete(`/admin/subscriptions/${id}`, {
onSuccess: () => {
// Props are re-loaded automatically by Inertia, updating state via useEffect
}
});
}
};
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);
const payload = {
name,
price: Number(price),
duration,
features: featuresArray,
max_contact_people: unlimitedContacts ? null : (maxContactPeople !== '' ? Number(maxContactPeople) : null),
unlimited_contacts: !!unlimitedContacts,
enable_job_apply: !!enableJobApply,
status
};
if (editingPlan) {
router.post(`/admin/subscriptions/${editingPlan.id}/update`, payload, {
onSuccess: () => {
setIsDialogOpen(false);
}
});
} else {
const newId = name.toLowerCase().replace(/\s+/g, '-');
router.post('/admin/subscriptions', {
id: newId || `plan-${Date.now()}`,
...payload
}, {
onSuccess: () => {
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">
{errors.status && (
<div className="p-4 bg-rose-50 border border-rose-200 text-rose-800 rounded-xl text-sm font-semibold flex items-center space-x-2">
<span className="w-2 h-2 bg-rose-500 rounded-full flex-shrink-0 animate-ping"></span>
<span>{errors.status}</span>
</div>
)}
{/* 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.unlimitedContacts ? 'Unlimited' : (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>
<div className="flex flex-col">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest w-fit ${
(plan.status || 'Active') === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600'
}`}>
{plan.status || 'Active'}
</span>
{plan.usageCount > 0 && (
<span className="text-[9px] font-bold text-slate-400 mt-1 uppercase tracking-wider">
{plan.usageCount} {plan.usageCount === 1 ? 'user' : 'users'}
</span>
)}
</div>
</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-[600px] 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">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Max Contact People</label>
<label className="flex items-center space-x-1.5 cursor-pointer select-none">
<input
type="checkbox"
checked={unlimitedContacts}
onChange={(e) => {
setUnlimitedContacts(e.target.checked);
if (e.target.checked) {
setMaxContactPeople('');
}
}}
className="w-3.5 h-3.5 text-teal-600 border-slate-300 rounded focus:ring-teal-500"
/>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Unlimited</span>
</label>
</div>
<input
type="number"
disabled={unlimitedContacts}
value={unlimitedContacts ? '' : maxContactPeople}
onChange={(e) => setMaxContactPeople(e.target.value)}
className={`w-full px-4 py-2.5 border rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none transition-all ${
unlimitedContacts
? 'bg-slate-100 border-slate-200 text-slate-400 cursor-not-allowed'
: 'bg-slate-50 border-slate-100 text-slate-800'
}`}
placeholder={unlimitedContacts ? 'Unlimited' : 'e.g. 10'}
/>
</div>
</div>
<div className="flex items-center justify-between bg-slate-50 p-3.5 rounded-xl border border-slate-100/50">
<div className="space-y-0.5">
<div className="flex items-center space-x-2">
<label className="text-xs font-bold text-slate-700 select-none">
Plan Status
</label>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider ${
status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600'
}`}>
{status}
</span>
</div>
<span className="block text-[10px] font-medium text-slate-400">
Active plans are visible to employers during registration.
</span>
{errors.status && (
<span className="text-xs font-bold text-rose-500 block mt-1">{errors.status}</span>
)}
</div>
<button
type="button"
disabled={editingPlan && editingPlan.usageCount > 0}
onClick={() => setStatus(status === 'Active' ? 'Disabled' : 'Active')}
className={`relative inline-flex h-6.5 w-12 items-center rounded-full transition-colors focus:outline-none focus:ring-4 focus:ring-teal-500/10 ${
status === 'Active' ? 'bg-[#0F6E56]' : 'bg-slate-200'
} ${editingPlan && editingPlan.usageCount > 0 ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<span
className={`inline-block h-4.5 w-4.5 transform rounded-full bg-white transition-transform duration-200 ${
status === 'Active' ? 'translate-x-6' : 'translate-x-1.5'
}`}
/>
</button>
</div>
{editingPlan && editingPlan.usageCount > 0 && (
<div className="p-3.5 bg-amber-50 border border-amber-200 text-amber-800 rounded-xl text-xs font-bold flex items-center space-x-2">
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full animate-pulse flex-shrink-0"></span>
<span>
Warning: {editingPlan.usageCount} {editingPlan.usageCount === 1 ? 'person is' : 'people are'} currently using this plan. Disabling this plan is blocked.
</span>
</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&#10;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>
);
}