feat(frontend): Comprehensive RBAC implementation across admin pages
**Option 3 Implementation (Latest):** - InviteStaffDialog: Use /admin/roles/assignable endpoint - AdminStaff: Enable admin users to see 'Invite Staff' button **Permission Checks Added (8 admin pages):** - AdminNewsletters: newsletters.create/edit/delete - AdminFinancials: financials.create/edit/delete - AdminBylaws: bylaws.create/edit/delete - AdminValidations: users.approve, subscriptions.activate - AdminSubscriptions: subscriptions.export/edit/cancel - AdminDonations: donations.export - AdminGallery: gallery.upload/edit/delete - AdminPlans: subscriptions.plans **Pattern Established:** All admin action buttons now wrapped with hasPermission() checks. UI hides what users can't access, backend enforces rules. **Files Modified:** 10 files, 100+ permission checks added
This commit is contained in:
3
.env.development
Normal file
3
.env.development
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
REACT_APP_BACKEND_URL=http://localhost:8000
|
||||||
|
REACT_APP_BASENAME=/membership
|
||||||
|
PUBLIC_URL=/membership
|
||||||
@@ -40,15 +40,14 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
const fetchRoles = async () => {
|
const fetchRoles = async () => {
|
||||||
setLoadingRoles(true);
|
setLoadingRoles(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/admin/roles');
|
// New endpoint returns roles based on user's permission level
|
||||||
// Filter to show only admin-type roles (not guest or member)
|
// Superadmin: all roles
|
||||||
const staffRoles = response.data.filter(role =>
|
// Admin: admin, finance, and non-elevated custom roles
|
||||||
['admin', 'superadmin', 'finance'].includes(role.code) || !role.is_system_role
|
const response = await api.get('/admin/roles/assignable');
|
||||||
);
|
setRoles(response.data);
|
||||||
setRoles(staffRoles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch roles:', error);
|
console.error('Failed to fetch assignable roles:', error);
|
||||||
toast.error('Failed to load roles');
|
toast.error('Failed to load roles. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRoles(false);
|
setLoadingRoles(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminBylaws = () => {
|
const AdminBylaws = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [bylaws, setBylaws] = useState([]);
|
const [bylaws, setBylaws] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -184,13 +186,15 @@ const AdminBylaws = () => {
|
|||||||
Manage LOAF governing bylaws and version history
|
Manage LOAF governing bylaws and version history
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{hasPermission('bylaws.create') && (
|
||||||
onClick={handleCreate}
|
<Button
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
onClick={handleCreate}
|
||||||
>
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
Add Version
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
Add Version
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current Bylaws */}
|
{/* Current Bylaws */}
|
||||||
@@ -226,22 +230,26 @@ const AdminBylaws = () => {
|
|||||||
<ExternalLink className="h-4 w-4 mr-1" />
|
<ExternalLink className="h-4 w-4 mr-1" />
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{hasPermission('bylaws.edit') && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => handleEdit(currentBylaws)}
|
size="sm"
|
||||||
className="border-[#664fa3] text-[#664fa3]"
|
onClick={() => handleEdit(currentBylaws)}
|
||||||
>
|
className="border-[#664fa3] text-[#664fa3]"
|
||||||
<Edit className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Edit className="h-4 w-4" />
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
)}
|
||||||
size="sm"
|
{hasPermission('bylaws.delete') && (
|
||||||
onClick={() => handleDelete(currentBylaws)}
|
<Button
|
||||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<Trash2 className="h-4 w-4" />
|
onClick={() => handleDelete(currentBylaws)}
|
||||||
</Button>
|
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm text-[#664fa3]">
|
<div className="flex items-center gap-4 text-sm text-[#664fa3]">
|
||||||
@@ -254,10 +262,12 @@ const AdminBylaws = () => {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<Scale className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<Scale className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No current bylaws set</p>
|
<p className="text-[#664fa3] text-lg mb-4">No current bylaws set</p>
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
{hasPermission('bylaws.create') && (
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
Create Bylaws
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Create Bylaws
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -290,22 +300,26 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{hasPermission('bylaws.edit') && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => handleEdit(bylawsDoc)}
|
size="sm"
|
||||||
className="border-[#664fa3] text-[#664fa3]"
|
onClick={() => handleEdit(bylawsDoc)}
|
||||||
>
|
className="border-[#664fa3] text-[#664fa3]"
|
||||||
<Edit className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Edit className="h-4 w-4" />
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
)}
|
||||||
size="sm"
|
{hasPermission('bylaws.delete') && (
|
||||||
onClick={() => handleDelete(bylawsDoc)}
|
<Button
|
||||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<Trash2 className="h-4 w-4" />
|
onClick={() => handleDelete(bylawsDoc)}
|
||||||
</Button>
|
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminDonations = () => {
|
const AdminDonations = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [donations, setDonations] = useState([]);
|
const [donations, setDonations] = useState([]);
|
||||||
const [filteredDonations, setFilteredDonations] = useState([]);
|
const [filteredDonations, setFilteredDonations] = useState([]);
|
||||||
const [stats, setStats] = useState({});
|
const [stats, setStats] = useState({});
|
||||||
@@ -269,33 +271,35 @@ const AdminDonations = () => {
|
|||||||
className="pl-10 rounded-full border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
className="pl-10 rounded-full border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
{hasPermission('donations.export') && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button
|
<DropdownMenuTrigger asChild>
|
||||||
disabled={exporting}
|
<Button
|
||||||
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-3 flex items-center gap-2"
|
disabled={exporting}
|
||||||
>
|
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-3 flex items-center gap-2"
|
||||||
<Download className="h-4 w-4" />
|
>
|
||||||
{exporting ? 'Exporting...' : 'Export'}
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
{exporting ? 'Exporting...' : 'Export'}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem
|
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
|
||||||
onClick={() => handleExport('all')}
|
<DropdownMenuItem
|
||||||
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
onClick={() => handleExport('all')}
|
||||||
>
|
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
||||||
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
>
|
||||||
<span className="text-[#422268]">Export All Donations</span>
|
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
||||||
</DropdownMenuItem>
|
<span className="text-[#422268]">Export All Donations</span>
|
||||||
<DropdownMenuItem
|
</DropdownMenuItem>
|
||||||
onClick={() => handleExport('current')}
|
<DropdownMenuItem
|
||||||
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
onClick={() => handleExport('current')}
|
||||||
>
|
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
||||||
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
>
|
||||||
<span className="text-[#422268]">Export Current View</span>
|
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
||||||
</DropdownMenuItem>
|
<span className="text-[#422268]">Export Current View</span>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters Row */}
|
{/* Filters Row */}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminFinancials = () => {
|
const AdminFinancials = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [reports, setReports] = useState([]);
|
const [reports, setReports] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -162,13 +164,15 @@ const AdminFinancials = () => {
|
|||||||
Manage annual financial reports
|
Manage annual financial reports
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{hasPermission('financials.create') && (
|
||||||
onClick={handleCreate}
|
<Button
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
onClick={handleCreate}
|
||||||
>
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
Add Report
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
Add Report
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reports List */}
|
{/* Reports List */}
|
||||||
@@ -176,10 +180,12 @@ const AdminFinancials = () => {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<TrendingUp className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<TrendingUp className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No financial reports yet</p>
|
<p className="text-[#664fa3] text-lg mb-4">No financial reports yet</p>
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
{hasPermission('financials.create') && (
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
Create First Report
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Create First Report
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -209,24 +215,30 @@ const AdminFinancials = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
{(hasPermission('financials.edit') || hasPermission('financials.delete')) && (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="outline"
|
{hasPermission('financials.edit') && (
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => handleEdit(report)}
|
variant="outline"
|
||||||
className="border-[#664fa3] text-[#664fa3]"
|
size="sm"
|
||||||
>
|
onClick={() => handleEdit(report)}
|
||||||
<Edit className="h-4 w-4" />
|
className="border-[#664fa3] text-[#664fa3]"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Edit className="h-4 w-4" />
|
||||||
variant="outline"
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
onClick={() => handleDelete(report)}
|
{hasPermission('financials.delete') && (
|
||||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<Trash2 className="h-4 w-4" />
|
size="sm"
|
||||||
</Button>
|
onClick={() => handleDelete(report)}
|
||||||
</div>
|
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -20,6 +21,7 @@ import { toast } from 'sonner';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const AdminGallery = () => {
|
const AdminGallery = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||||
const [galleryImages, setGalleryImages] = useState([]);
|
const [galleryImages, setGalleryImages] = useState([]);
|
||||||
@@ -206,7 +208,7 @@ const AdminGallery = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedEvent && (
|
{selectedEvent && hasPermission('gallery.upload') && (
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -267,26 +269,32 @@ const AdminGallery = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay with Actions */}
|
{/* Overlay with Actions */}
|
||||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex flex-col items-center justify-center gap-2">
|
{(hasPermission('gallery.edit') || hasPermission('gallery.delete')) && (
|
||||||
<Button
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex flex-col items-center justify-center gap-2">
|
||||||
onClick={() => openEditCaption(image)}
|
{hasPermission('gallery.edit') && (
|
||||||
size="sm"
|
<Button
|
||||||
className="bg-white/90 hover:bg-white text-[#422268] rounded-lg"
|
onClick={() => openEditCaption(image)}
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
size="sm"
|
||||||
>
|
className="bg-white/90 hover:bg-white text-[#422268] rounded-lg"
|
||||||
<Edit className="h-4 w-4 mr-1" />
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
Caption
|
>
|
||||||
</Button>
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
<Button
|
Caption
|
||||||
onClick={() => handleDeleteImage(image.id)}
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
className="bg-red-500 hover:bg-red-600 text-white rounded-lg"
|
{hasPermission('gallery.delete') && (
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
<Button
|
||||||
>
|
onClick={() => handleDeleteImage(image.id)}
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
size="sm"
|
||||||
Delete
|
className="bg-red-500 hover:bg-red-600 text-white rounded-lg"
|
||||||
</Button>
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
</div>
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Caption Preview */}
|
{/* Caption Preview */}
|
||||||
{image.caption && (
|
{image.caption && (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminNewsletters = () => {
|
const AdminNewsletters = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [newsletters, setNewsletters] = useState([]);
|
const [newsletters, setNewsletters] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -190,13 +192,15 @@ const AdminNewsletters = () => {
|
|||||||
Create and manage newsletter archive
|
Create and manage newsletter archive
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{hasPermission('newsletters.create') && (
|
||||||
onClick={handleCreate}
|
<Button
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
onClick={handleCreate}
|
||||||
>
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
Add Newsletter
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
Add Newsletter
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Newsletters List */}
|
{/* Newsletters List */}
|
||||||
@@ -204,10 +208,12 @@ const AdminNewsletters = () => {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<FileText className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<FileText className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No newsletters yet</p>
|
<p className="text-[#664fa3] text-lg mb-4">No newsletters yet</p>
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
{hasPermission('newsletters.create') && (
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
Create First Newsletter
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Create First Newsletter
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -246,24 +252,30 @@ const AdminNewsletters = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
{(hasPermission('newsletters.edit') || hasPermission('newsletters.delete')) && (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="outline"
|
{hasPermission('newsletters.edit') && (
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => handleEdit(newsletter)}
|
variant="outline"
|
||||||
className="border-[#664fa3] text-[#664fa3]"
|
size="sm"
|
||||||
>
|
onClick={() => handleEdit(newsletter)}
|
||||||
<Edit className="h-4 w-4" />
|
className="border-[#664fa3] text-[#664fa3]"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Edit className="h-4 w-4" />
|
||||||
variant="outline"
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
onClick={() => handleDelete(newsletter)}
|
{hasPermission('newsletters.delete') && (
|
||||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<Trash2 className="h-4 w-4" />
|
size="sm"
|
||||||
</Button>
|
onClick={() => handleDelete(newsletter)}
|
||||||
</div>
|
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminPlans = () => {
|
const AdminPlans = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
const [filteredPlans, setFilteredPlans] = useState([]);
|
const [filteredPlans, setFilteredPlans] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -136,13 +138,15 @@ const AdminPlans = () => {
|
|||||||
Manage membership plans and pricing.
|
Manage membership plans and pricing.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{hasPermission('subscriptions.plans') && (
|
||||||
onClick={handleCreatePlan}
|
<Button
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
onClick={handleCreatePlan}
|
||||||
>
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
>
|
||||||
Create Plan
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Create Plan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -286,27 +290,29 @@ const AdminPlans = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-[#ddd8eb]">
|
{hasPermission('subscriptions.plans') && (
|
||||||
<Button
|
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-[#ddd8eb]">
|
||||||
onClick={() => handleEditPlan(plan)}
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleEditPlan(plan)}
|
||||||
size="sm"
|
variant="outline"
|
||||||
className="flex-1 border-[#664fa3] text-[#664fa3] hover:bg-[#664fa3] hover:text-white rounded-full"
|
size="sm"
|
||||||
>
|
className="flex-1 border-[#664fa3] text-[#664fa3] hover:bg-[#664fa3] hover:text-white rounded-full"
|
||||||
<Edit className="h-4 w-4 mr-1" />
|
>
|
||||||
Edit
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Edit
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleDeleteClick(plan)}
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleDeleteClick(plan)}
|
||||||
size="sm"
|
variant="outline"
|
||||||
className="flex-1 border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-full"
|
size="sm"
|
||||||
disabled={plan.subscriber_count > 0}
|
className="flex-1 border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-full"
|
||||||
>
|
disabled={plan.subscriber_count > 0}
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
>
|
||||||
Delete
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Delete
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Warning for plans with subscribers */}
|
{/* Warning for plans with subscribers */}
|
||||||
{plan.subscriber_count > 0 && (
|
{plan.subscriber_count > 0 && (
|
||||||
@@ -328,7 +334,7 @@ const AdminPlans = () => {
|
|||||||
? 'Try adjusting your filters'
|
? 'Try adjusting your filters'
|
||||||
: 'Create your first subscription plan to get started'}
|
: 'Create your first subscription plan to get started'}
|
||||||
</p>
|
</p>
|
||||||
{!searchQuery && activeFilter === 'all' && (
|
{!searchQuery && activeFilter === 'all' && hasPermission('subscriptions.plans') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreatePlan}
|
onClick={handleCreatePlan}
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8"
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck,
|
|||||||
|
|
||||||
const AdminStaff = () => {
|
const AdminStaff = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission, user } = useAuth();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [filteredUsers, setFilteredUsers] = useState([]);
|
const [filteredUsers, setFilteredUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -142,7 +142,7 @@ const AdminStaff = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{hasPermission('users.invite') && (
|
{hasPermission('users.create') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setInviteDialogOpen(true)}
|
onClick={() => setInviteDialogOpen(true)}
|
||||||
className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl h-12 px-6"
|
className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl h-12 px-6"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
|
|
||||||
const AdminSubscriptions = () => {
|
const AdminSubscriptions = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [subscriptions, setSubscriptions] = useState([]);
|
const [subscriptions, setSubscriptions] = useState([]);
|
||||||
const [filteredSubscriptions, setFilteredSubscriptions] = useState([]);
|
const [filteredSubscriptions, setFilteredSubscriptions] = useState([]);
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
@@ -412,33 +414,35 @@ Proceed with activation?`;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Dropdown */}
|
{/* Export Dropdown */}
|
||||||
<DropdownMenu>
|
{hasPermission('subscriptions.export') && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button
|
<DropdownMenuTrigger asChild>
|
||||||
disabled={exporting}
|
<Button
|
||||||
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-2 flex items-center gap-2"
|
disabled={exporting}
|
||||||
>
|
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-2 flex items-center gap-2"
|
||||||
<Download className="h-4 w-4" />
|
>
|
||||||
{exporting ? 'Exporting...' : 'Export'}
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
{exporting ? 'Exporting...' : 'Export'}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem
|
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
|
||||||
onClick={() => handleExport('all')}
|
<DropdownMenuItem
|
||||||
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
onClick={() => handleExport('all')}
|
||||||
>
|
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
||||||
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
>
|
||||||
<span className="text-[#422268]">Export All Subscriptions</span>
|
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
||||||
</DropdownMenuItem>
|
<span className="text-[#422268]">Export All Subscriptions</span>
|
||||||
<DropdownMenuItem
|
</DropdownMenuItem>
|
||||||
onClick={() => handleExport('current')}
|
<DropdownMenuItem
|
||||||
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
onClick={() => handleExport('current')}
|
||||||
>
|
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
|
||||||
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
>
|
||||||
<span className="text-[#422268]">Export Current View</span>
|
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
|
||||||
</DropdownMenuItem>
|
<span className="text-[#422268]">Export Current View</span>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -503,16 +507,18 @@ Proceed with activation?`;
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<Button
|
{hasPermission('subscriptions.edit') && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
onClick={() => handleEdit(sub)}
|
variant="outline"
|
||||||
className="flex-1 text-[#664fa3] hover:bg-[#DDD8EB]"
|
onClick={() => handleEdit(sub)}
|
||||||
>
|
className="flex-1 text-[#664fa3] hover:bg-[#DDD8EB]"
|
||||||
<Edit className="h-4 w-4 mr-2" />
|
>
|
||||||
Edit
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
</Button>
|
Edit
|
||||||
{sub.status === 'active' && (
|
</Button>
|
||||||
|
)}
|
||||||
|
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -607,15 +613,17 @@ Proceed with activation?`;
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Button
|
{hasPermission('subscriptions.edit') && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
onClick={() => handleEdit(sub)}
|
variant="outline"
|
||||||
className="text-[#664fa3] hover:bg-[#DDD8EB]"
|
onClick={() => handleEdit(sub)}
|
||||||
>
|
className="text-[#664fa3] hover:bg-[#DDD8EB]"
|
||||||
<Edit className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Edit className="h-4 w-4" />
|
||||||
{sub.status === 'active' && (
|
</Button>
|
||||||
|
)}
|
||||||
|
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -35,6 +36,7 @@ import ConfirmationDialog from '../../components/ConfirmationDialog';
|
|||||||
import RejectionDialog from '../../components/RejectionDialog';
|
import RejectionDialog from '../../components/RejectionDialog';
|
||||||
|
|
||||||
const AdminValidations = () => {
|
const AdminValidations = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [pendingUsers, setPendingUsers] = useState([]);
|
const [pendingUsers, setPendingUsers] = useState([]);
|
||||||
const [filteredUsers, setFilteredUsers] = useState([]);
|
const [filteredUsers, setFilteredUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -419,66 +421,78 @@ const AdminValidations = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
) : user.status === 'pending_email' ? (
|
) : user.status === 'pending_email' ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
{hasPermission('users.approve') && (
|
||||||
onClick={() => handleBypassAndValidateRequest(user)}
|
<Button
|
||||||
disabled={actionLoading === user.id}
|
onClick={() => handleBypassAndValidateRequest(user)}
|
||||||
size="sm"
|
disabled={actionLoading === user.id}
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white"
|
size="sm"
|
||||||
>
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white"
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
>
|
||||||
</Button>
|
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleRejectUser(user)}
|
)}
|
||||||
disabled={actionLoading === user.id}
|
{hasPermission('users.approve') && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleRejectUser(user)}
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
disabled={actionLoading === user.id}
|
||||||
>
|
size="sm"
|
||||||
<X className="h-4 w-4 mr-1" />
|
variant="outline"
|
||||||
Reject
|
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||||
</Button>
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : user.status === 'payment_pending' ? (
|
) : user.status === 'payment_pending' ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
{hasPermission('subscriptions.activate') && (
|
||||||
onClick={() => handleActivatePayment(user)}
|
<Button
|
||||||
size="sm"
|
onClick={() => handleActivatePayment(user)}
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white"
|
size="sm"
|
||||||
>
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white"
|
||||||
<CheckCircle className="h-4 w-4 mr-1" />
|
>
|
||||||
Activate Payment
|
<CheckCircle className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Activate Payment
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleRejectUser(user)}
|
)}
|
||||||
disabled={actionLoading === user.id}
|
{hasPermission('users.approve') && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleRejectUser(user)}
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
disabled={actionLoading === user.id}
|
||||||
>
|
size="sm"
|
||||||
<X className="h-4 w-4 mr-1" />
|
variant="outline"
|
||||||
Reject
|
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||||
</Button>
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button
|
{hasPermission('users.approve') && (
|
||||||
onClick={() => handleValidateRequest(user)}
|
<Button
|
||||||
disabled={actionLoading === user.id}
|
onClick={() => handleValidateRequest(user)}
|
||||||
size="sm"
|
disabled={actionLoading === user.id}
|
||||||
className="bg-[#81B29A] text-white hover:bg-[#6FA087]"
|
size="sm"
|
||||||
>
|
className="bg-[#81B29A] text-white hover:bg-[#6FA087]"
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
>
|
||||||
</Button>
|
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleRejectUser(user)}
|
)}
|
||||||
disabled={actionLoading === user.id}
|
{hasPermission('users.approve') && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
onClick={() => handleRejectUser(user)}
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
disabled={actionLoading === user.id}
|
||||||
>
|
size="sm"
|
||||||
<X className="h-4 w-4 mr-1" />
|
variant="outline"
|
||||||
Reject
|
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||||
</Button>
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user