forked from andika/membership-fe
Donation page update and Subscription update on Admin Dashboard
This commit is contained in:
16
src/App.js
16
src/App.js
@@ -24,6 +24,7 @@ import AdminMembers from './pages/admin/AdminMembers';
|
|||||||
import AdminEvents from './pages/admin/AdminEvents';
|
import AdminEvents from './pages/admin/AdminEvents';
|
||||||
import AdminApprovals from './pages/admin/AdminApprovals';
|
import AdminApprovals from './pages/admin/AdminApprovals';
|
||||||
import AdminPlans from './pages/admin/AdminPlans';
|
import AdminPlans from './pages/admin/AdminPlans';
|
||||||
|
import AdminSubscriptions from './pages/admin/AdminSubscriptions';
|
||||||
import AdminLayout from './layouts/AdminLayout';
|
import AdminLayout from './layouts/AdminLayout';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
import MemberRoute from './components/MemberRoute';
|
import MemberRoute from './components/MemberRoute';
|
||||||
@@ -42,6 +43,7 @@ import History from './pages/History';
|
|||||||
import MissionValues from './pages/MissionValues';
|
import MissionValues from './pages/MissionValues';
|
||||||
import BoardOfDirectors from './pages/BoardOfDirectors';
|
import BoardOfDirectors from './pages/BoardOfDirectors';
|
||||||
import Donate from './pages/Donate';
|
import Donate from './pages/Donate';
|
||||||
|
import DonationSuccess from './pages/DonationSuccess';
|
||||||
|
|
||||||
const PrivateRoute = ({ children, adminOnly = false }) => {
|
const PrivateRoute = ({ children, adminOnly = false }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
@@ -77,7 +79,11 @@ function App() {
|
|||||||
<ChangePasswordRequired />
|
<ChangePasswordRequired />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/plans" element={<Plans />} />
|
<Route path="/plans" element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Plans />
|
||||||
|
</PrivateRoute>
|
||||||
|
} />
|
||||||
<Route path="/become-a-member" element={<BecomeMember />} />
|
<Route path="/become-a-member" element={<BecomeMember />} />
|
||||||
<Route path="/payment-success" element={<PaymentSuccess />} />
|
<Route path="/payment-success" element={<PaymentSuccess />} />
|
||||||
<Route path="/payment-cancel" element={<PaymentCancel />} />
|
<Route path="/payment-cancel" element={<PaymentCancel />} />
|
||||||
@@ -89,6 +95,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Donation Page - Public Access */}
|
{/* Donation Page - Public Access */}
|
||||||
<Route path="/donate" element={<Donate />} />
|
<Route path="/donate" element={<Donate />} />
|
||||||
|
<Route path="/donation-success" element={<DonationSuccess />} />
|
||||||
|
|
||||||
<Route path="/dashboard" element={
|
<Route path="/dashboard" element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
@@ -209,6 +216,13 @@ function App() {
|
|||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/admin/subscriptions" element={
|
||||||
|
<PrivateRoute adminOnly>
|
||||||
|
<AdminLayout>
|
||||||
|
<AdminSubscriptions />
|
||||||
|
</AdminLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
} />
|
||||||
<Route path="/admin/gallery" element={
|
<Route path="/admin/gallery" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Scale,
|
Scale,
|
||||||
HardDrive
|
HardDrive,
|
||||||
|
Repeat
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||||
@@ -116,6 +117,12 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
path: '/admin/plans',
|
path: '/admin/plans',
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Subscriptions',
|
||||||
|
icon: Repeat,
|
||||||
|
path: '/admin/subscriptions',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Events',
|
name: 'Events',
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
|
|||||||
@@ -1,14 +1,59 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PublicNavbar from '../components/PublicNavbar';
|
import PublicNavbar from '../components/PublicNavbar';
|
||||||
import PublicFooter from '../components/PublicFooter';
|
import PublicFooter from '../components/PublicFooter';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { CreditCard, Mail } from 'lucide-react';
|
import { Input } from '../components/ui/input';
|
||||||
|
import { Label } from '../components/ui/label';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '../components/ui/dialog';
|
||||||
|
import { CreditCard, Mail, Heart, Loader2 } from 'lucide-react';
|
||||||
|
import api from '../utils/api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const Donate = () => {
|
const Donate = () => {
|
||||||
const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`;
|
const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`;
|
||||||
const zelleLogo = `${process.env.PUBLIC_URL}/zelle-logo.png`;
|
const zelleLogo = `${process.env.PUBLIC_URL}/zelle-logo.png`;
|
||||||
|
|
||||||
|
const [customAmountDialogOpen, setCustomAmountDialogOpen] = useState(false);
|
||||||
|
const [customAmount, setCustomAmount] = useState('');
|
||||||
|
const [processingAmount, setProcessingAmount] = useState(null);
|
||||||
|
|
||||||
|
const handleDonateAmount = async (amountCents) => {
|
||||||
|
setProcessingAmount(amountCents);
|
||||||
|
try {
|
||||||
|
const response = await api.post('/donations/checkout', {
|
||||||
|
amount_cents: amountCents
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to Stripe Checkout
|
||||||
|
window.location.href = response.data.checkout_url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to process donation:', error);
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to process donation. Please try again.');
|
||||||
|
setProcessingAmount(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomDonate = () => {
|
||||||
|
const amount = parseFloat(customAmount);
|
||||||
|
|
||||||
|
if (!customAmount || isNaN(amount) || amount < 1) {
|
||||||
|
toast.error('Please enter a valid amount (minimum $1.00)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountCents = Math.round(amount * 100);
|
||||||
|
setCustomAmountDialogOpen(false);
|
||||||
|
handleDonateAmount(amountCents);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
@@ -44,22 +89,33 @@ const Donate = () => {
|
|||||||
{/* Donation Buttons Grid */}
|
{/* Donation Buttons Grid */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
{[25, 50, 100, 250].map(amount => (
|
{[25, 50, 100, 250].map(amount => (
|
||||||
<Button key={amount}
|
<Button
|
||||||
className="bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full"
|
key={amount}
|
||||||
disabled>
|
onClick={() => handleDonateAmount(amount * 100)}
|
||||||
${amount}
|
disabled={processingAmount === amount * 100}
|
||||||
|
className="bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{processingAmount === amount * 100 ? (
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
) : (
|
||||||
|
`$${amount}`
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Amount Button */}
|
{/* Custom Amount Button */}
|
||||||
<Button className="w-full bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full"
|
<Button
|
||||||
disabled>
|
onClick={() => setCustomAmountDialogOpen(true)}
|
||||||
|
disabled={processingAmount !== null}
|
||||||
|
className="w-full bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Heart className="h-6 w-6" />
|
||||||
Donate Any Amount
|
Donate Any Amount
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-sm text-[#48286e] text-center mt-4 opacity-75" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-sm text-[#664fa3] text-center mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Online donations coming soon
|
Secure donation processing powered by Stripe
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +162,76 @@ const Donate = () => {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
|
|
||||||
|
{/* Custom Amount Dialog */}
|
||||||
|
<Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[450px] bg-white rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Enter Donation Amount
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Choose how much you'd like to donate to support our community
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="customAmount" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Amount (USD)
|
||||||
|
</Label>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[#664fa3] text-xl font-semibold">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
id="customAmount"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="1.00"
|
||||||
|
value={customAmount}
|
||||||
|
onChange={(e) => setCustomAmount(e.target.value)}
|
||||||
|
placeholder="50.00"
|
||||||
|
className="pl-10 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-xl"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleCustomDonate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Minimum donation: $1.00
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#f1eef9] rounded-lg p-4">
|
||||||
|
<p className="text-sm text-[#422268] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<strong>Thank you for supporting LOAF!</strong><br />
|
||||||
|
Your donation helps us continue our mission and provide meaningful experiences for our community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCustomAmountDialogOpen(false)}
|
||||||
|
className="rounded-full border-2 border-[#ddd8eb]"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCustomDonate}
|
||||||
|
className="bg-[#664fa3] text-white hover:bg-[#48286e] rounded-full"
|
||||||
|
>
|
||||||
|
Continue to Payment
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
102
src/pages/DonationSuccess.js
Normal file
102
src/pages/DonationSuccess.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PublicNavbar from '../components/PublicNavbar';
|
||||||
|
import PublicFooter from '../components/PublicFooter';
|
||||||
|
import { Card } from '../components/ui/card';
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
import { CheckCircle, Heart } from 'lucide-react';
|
||||||
|
|
||||||
|
const DonationSuccess = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<PublicNavbar />
|
||||||
|
|
||||||
|
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-6 py-20">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<Card className="p-12 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-xl text-center">
|
||||||
|
{/* Success Icon */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<img
|
||||||
|
src={loafHearts}
|
||||||
|
alt="Hearts"
|
||||||
|
className="w-32 h-auto"
|
||||||
|
onError={(e) => e.target.style.display = 'none'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-[#81B29A]/10 rounded-full mb-6">
|
||||||
|
<CheckCircle className="h-12 w-12 text-[#81B29A]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1 className="text-4xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Thank You for Your Donation!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className="space-y-4 mb-8">
|
||||||
|
<p className="text-xl text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Your generous contribution helps support our community and continue our mission.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-r from-[#f1eef9] to-[#DDD8EB]/30 rounded-2xl p-6 border-2 border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-[#ff9e77] mb-2">
|
||||||
|
<Heart className="h-6 w-6" />
|
||||||
|
<span className="text-lg font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Your Support Makes a Difference
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
A receipt for your donation has been sent to your email address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-base text-[#664fa3] pt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
We deeply appreciate your support and commitment to LOAF's mission of building a vibrant, inclusive community.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full px-8 py-6 text-lg font-medium shadow-lg"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Return to Home
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/donate')}
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#DDD8EB]/20 rounded-full px-8 py-6 text-lg font-medium"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Make Another Donation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Additional Info */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<p className="text-sm text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Have questions about your donation?
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:support@loaf.org"
|
||||||
|
className="text-[#ff9e77] hover:text-[#664fa3] font-medium transition-colors"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Contact us at support@loaf.org
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<PublicFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DonationSuccess;
|
||||||
@@ -15,15 +15,16 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '../components/ui/dialog';
|
} from '../components/ui/dialog';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import { CheckCircle, CreditCard, Loader2, Heart } from 'lucide-react';
|
import { CheckCircle, CreditCard, Loader2, Heart, AlertCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const Plans = () => {
|
const Plans = () => {
|
||||||
const { user } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [processingPlanId, setProcessingPlanId] = useState(null);
|
const [processingPlanId, setProcessingPlanId] = useState(null);
|
||||||
|
const [statusInfo, setStatusInfo] = useState(null);
|
||||||
|
|
||||||
// Amount selection dialog state
|
// Amount selection dialog state
|
||||||
const [amountDialogOpen, setAmountDialogOpen] = useState(false);
|
const [amountDialogOpen, setAmountDialogOpen] = useState(false);
|
||||||
@@ -34,6 +35,65 @@ const Plans = () => {
|
|||||||
fetchPlans();
|
fetchPlans();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Status-based access control
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && user) {
|
||||||
|
// Define status-to-message mapping
|
||||||
|
const statusMessages = {
|
||||||
|
pending_email: {
|
||||||
|
title: "Email Verification Required",
|
||||||
|
message: "Please verify your email address before selecting a membership plan. Check your inbox for the verification link.",
|
||||||
|
action: null,
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: false
|
||||||
|
},
|
||||||
|
pending_approval: {
|
||||||
|
title: "Application Under Review",
|
||||||
|
message: "Your application is being reviewed by our admin team. You'll receive an email once approved to proceed with payment.",
|
||||||
|
action: null,
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: false
|
||||||
|
},
|
||||||
|
pre_approved: {
|
||||||
|
title: "Application Under Review",
|
||||||
|
message: "Your application is being reviewed by our admin team. You'll receive an email once approved to proceed with payment.",
|
||||||
|
action: null,
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: false
|
||||||
|
},
|
||||||
|
payment_pending: {
|
||||||
|
title: null,
|
||||||
|
message: null,
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: true
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
title: "Already a Member",
|
||||||
|
message: "You already have an active membership. Visit your dashboard to view your membership details.",
|
||||||
|
action: "Go to Dashboard",
|
||||||
|
actionLink: "/dashboard",
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: false
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
title: "Membership Inactive",
|
||||||
|
message: "Your membership has expired. Please select a plan below to renew your membership.",
|
||||||
|
action: null,
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const userStatus = statusMessages[user.status];
|
||||||
|
setStatusInfo(userStatus || {
|
||||||
|
title: "Access Restricted",
|
||||||
|
message: "Please contact support for assistance with your account.",
|
||||||
|
canView: true,
|
||||||
|
canSubscribe: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user, authLoading]);
|
||||||
|
|
||||||
const fetchPlans = async () => {
|
const fetchPlans = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/subscriptions/plans');
|
const response = await api.get('/subscriptions/plans');
|
||||||
@@ -141,6 +201,31 @@ const Plans = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status Banner */}
|
||||||
|
{statusInfo && statusInfo.title && (
|
||||||
|
<Card className="max-w-3xl mx-auto mb-8 p-6 bg-gradient-to-r from-[#f1eef9] to-[#DDD8EB]/30 border-2 border-[#664fa3]">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<AlertCircle className="h-6 w-6 text-[#664fa3] flex-shrink-0 mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{statusInfo.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{statusInfo.message}
|
||||||
|
</p>
|
||||||
|
{statusInfo.action && statusInfo.actionLink && (
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(statusInfo.actionLink)}
|
||||||
|
className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full"
|
||||||
|
>
|
||||||
|
{statusInfo.action}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
<Loader2 className="h-12 w-12 text-[#664fa3] mx-auto mb-4 animate-spin" />
|
<Loader2 className="h-12 w-12 text-[#664fa3] mx-auto mb-4 animate-spin" />
|
||||||
@@ -220,8 +305,8 @@ const Plans = () => {
|
|||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleSelectPlan(plan)}
|
onClick={() => handleSelectPlan(plan)}
|
||||||
disabled={processingPlanId === plan.id}
|
disabled={processingPlanId === plan.id || (statusInfo && !statusInfo.canSubscribe)}
|
||||||
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-semibold"
|
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
data-testid={`subscribe-button-${plan.id}`}
|
data-testid={`subscribe-button-${plan.id}`}
|
||||||
>
|
>
|
||||||
{processingPlanId === plan.id ? (
|
{processingPlanId === plan.id ? (
|
||||||
@@ -229,6 +314,8 @@ const Plans = () => {
|
|||||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
Processing...
|
Processing...
|
||||||
</>
|
</>
|
||||||
|
) : statusInfo && !statusInfo.canSubscribe ? (
|
||||||
|
'Approval Required'
|
||||||
) : (
|
) : (
|
||||||
'Choose Amount & Subscribe'
|
'Choose Amount & Subscribe'
|
||||||
)}
|
)}
|
||||||
|
|||||||
506
src/pages/admin/AdminSubscriptions.js
Normal file
506
src/pages/admin/AdminSubscriptions.js
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import { Button } from '../../components/ui/button';
|
||||||
|
import { Input } from '../../components/ui/input';
|
||||||
|
import { Label } from '../../components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '../../components/ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '../../components/ui/dialog';
|
||||||
|
import { Badge } from '../../components/ui/badge';
|
||||||
|
import api from '../../utils/api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
DollarSign,
|
||||||
|
CreditCard,
|
||||||
|
TrendingUp,
|
||||||
|
Heart,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
Calendar,
|
||||||
|
Edit,
|
||||||
|
XCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const AdminSubscriptions = () => {
|
||||||
|
const [subscriptions, setSubscriptions] = useState([]);
|
||||||
|
const [filteredSubscriptions, setFilteredSubscriptions] = useState([]);
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [stats, setStats] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [planFilter, setPlanFilter] = useState('all');
|
||||||
|
|
||||||
|
// Edit subscription dialog state
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [selectedSubscription, setSelectedSubscription] = useState(null);
|
||||||
|
const [editFormData, setEditFormData] = useState({
|
||||||
|
status: '',
|
||||||
|
end_date: ''
|
||||||
|
});
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filterSubscriptions();
|
||||||
|
}, [searchQuery, statusFilter, planFilter, subscriptions]);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [subsResponse, statsResponse, plansResponse] = await Promise.all([
|
||||||
|
api.get('/admin/subscriptions'),
|
||||||
|
api.get('/admin/subscriptions/stats'),
|
||||||
|
api.get('/admin/subscriptions/plans')
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSubscriptions(subsResponse.data);
|
||||||
|
setStats(statsResponse.data);
|
||||||
|
setPlans(plansResponse.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch subscription data:', error);
|
||||||
|
toast.error('Failed to load subscription data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterSubscriptions = () => {
|
||||||
|
let filtered = [...subscriptions];
|
||||||
|
|
||||||
|
// Search filter (member name or email)
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.filter(sub =>
|
||||||
|
sub.user.first_name.toLowerCase().includes(query) ||
|
||||||
|
sub.user.last_name.toLowerCase().includes(query) ||
|
||||||
|
sub.user.email.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(sub => sub.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan filter
|
||||||
|
if (planFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(sub => sub.plan.id === planFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredSubscriptions(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (subscription) => {
|
||||||
|
setSelectedSubscription(subscription);
|
||||||
|
setEditFormData({
|
||||||
|
status: subscription.status,
|
||||||
|
end_date: subscription.end_date ? new Date(subscription.end_date).toISOString().split('T')[0] : ''
|
||||||
|
});
|
||||||
|
setEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSubscription = async () => {
|
||||||
|
if (!selectedSubscription) return;
|
||||||
|
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
await api.put(`/admin/subscriptions/${selectedSubscription.id}`, {
|
||||||
|
status: editFormData.status,
|
||||||
|
end_date: editFormData.end_date ? new Date(editFormData.end_date).toISOString() : null
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Subscription updated successfully');
|
||||||
|
setEditDialogOpen(false);
|
||||||
|
fetchData(); // Refresh data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update subscription:', error);
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to update subscription');
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelSubscription = async (subscriptionId) => {
|
||||||
|
if (!window.confirm('Are you sure you want to cancel this subscription? This will also set the user status to inactive.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post(`/admin/subscriptions/${subscriptionId}/cancel`);
|
||||||
|
toast.success('Subscription cancelled successfully');
|
||||||
|
fetchData(); // Refresh data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to cancel subscription:', error);
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to cancel subscription');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (cents) => {
|
||||||
|
return `$${(cents / 100).toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return 'N/A';
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadgeVariant = (status) => {
|
||||||
|
const variants = {
|
||||||
|
active: 'default',
|
||||||
|
cancelled: 'destructive',
|
||||||
|
expired: 'secondary'
|
||||||
|
};
|
||||||
|
return variants[status] || 'outline';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-[#664fa3]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Subscription Management
|
||||||
|
</h1>
|
||||||
|
<p className="text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
View and manage all member subscriptions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid md:grid-cols-4 gap-6">
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Total Subscriptions
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-[#422268] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{stats.total || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#DDD8EB]/20 rounded-full">
|
||||||
|
<CreditCard className="h-6 w-6 text-[#664fa3]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Active Members
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-[#81B29A] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{stats.active || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#81B29A]/10 rounded-full">
|
||||||
|
<TrendingUp className="h-6 w-6 text-[#81B29A]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Total Revenue
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-[#422268] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatPrice(stats.total_revenue || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#DDD8EB]/20 rounded-full">
|
||||||
|
<DollarSign className="h-6 w-6 text-[#664fa3]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Total Donations
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-[#ff9e77] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatPrice(stats.total_donations || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#ff9e77]/10 rounded-full">
|
||||||
|
<Heart className="h-6 w-6 text-[#ff9e77]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search & Filter Bar */}
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="md:col-span-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name or email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
|
<div>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
|
||||||
|
<SelectValue placeholder="All Statuses" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Statuses</SelectItem>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||||
|
<SelectItem value="expired">Expired</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plan Filter */}
|
||||||
|
<div>
|
||||||
|
<Select value={planFilter} onValueChange={setPlanFilter}>
|
||||||
|
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
|
||||||
|
<SelectValue placeholder="All Plans" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Plans</SelectItem>
|
||||||
|
{plans.map(plan => (
|
||||||
|
<SelectItem key={plan.id} value={plan.id}>{plan.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Showing {filteredSubscriptions.length} of {subscriptions.length} subscriptions
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Subscriptions Table */}
|
||||||
|
<Card className="bg-white rounded-2xl border-2 border-[#ddd8eb] overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#DDD8EB]/20 border-b border-[#ddd8eb]">
|
||||||
|
<th className="text-left p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Member
|
||||||
|
</th>
|
||||||
|
<th className="text-left p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Plan
|
||||||
|
</th>
|
||||||
|
<th className="text-left p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="text-left p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Period
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Base Fee
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Donation
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Total
|
||||||
|
</th>
|
||||||
|
<th className="text-center p-4 text-[#422268] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredSubscriptions.length > 0 ? (
|
||||||
|
filteredSubscriptions.map((sub) => (
|
||||||
|
<tr key={sub.id} className="border-b border-[#ddd8eb] hover:bg-[#f9f5ff] transition-colors">
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="font-medium text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{sub.user.first_name} {sub.user.last_name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{sub.user.email}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{sub.plan.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[#664fa3]">
|
||||||
|
{sub.plan.billing_cycle}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<Badge variant={getStatusBadgeVariant(sub.status)}>
|
||||||
|
{sub.status}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<div>{formatDate(sub.start_date)}</div>
|
||||||
|
<div className="text-xs text-[#664fa3]">to {formatDate(sub.end_date)}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatPrice(sub.base_subscription_cents || 0)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right text-[#ff9e77]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatPrice(sub.donation_cents || 0)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatPrice(sub.amount_paid_cents || 0)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleEdit(sub)}
|
||||||
|
className="text-[#664fa3] hover:bg-[#DDD8EB]"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{sub.status === 'active' && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCancelSubscription(sub.id)}
|
||||||
|
className="text-red-600 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<XCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="8" className="p-12 text-center text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
No subscriptions found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Edit Subscription Dialog */}
|
||||||
|
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-white rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Edit Subscription
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Update subscription status or end date for {selectedSubscription?.user.first_name} {selectedSubscription?.user.last_name}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Status */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="status" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={editFormData.status}
|
||||||
|
onValueChange={(value) => setEditFormData({ ...editFormData, status: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||||
|
<SelectItem value="expired">Expired</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End Date */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="end_date" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
End Date
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
||||||
|
<Input
|
||||||
|
id="end_date"
|
||||||
|
type="date"
|
||||||
|
value={editFormData.end_date}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, end_date: e.target.value })}
|
||||||
|
className="pl-12 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setEditDialogOpen(false)}
|
||||||
|
className="rounded-full border-2 border-[#ddd8eb]"
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveSubscription}
|
||||||
|
disabled={isUpdating}
|
||||||
|
className="bg-[#81B29A] text-white hover:bg-[#6FA087] rounded-full"
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save Changes'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminSubscriptions;
|
||||||
Reference in New Issue
Block a user