Merge from Dev to LOAF Production #13

Merged
andika merged 14 commits from dev into loaf-prod 2026-01-07 08:44:10 +00:00
11 changed files with 368 additions and 288 deletions
Showing only changes of commit f71931d4a7 - Show all commits

3
.env.development Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_BACKEND_URL=http://localhost:8000
REACT_APP_BASENAME=/membership
PUBLIC_URL=/membership

View File

@@ -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);
} }

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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>
))} ))}

View File

@@ -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 && (

View File

@@ -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>
))} ))}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>