Files
sasa-membership/frontend/src/components/MembershipSetup.tsx
T
nathanb d024bf7fa3 stuff changed:
- ui has been made 'kinda better' (after making it worse for a while lol
- ESP rfid readers are now supported [ill upload the code for them in another repo later]
- admin system has been secured a bit better and seems to be working well
2026-05-08 20:46:58 +01:00

343 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { membershipService, paymentService, MembershipTier, MembershipCreateData, PaymentCreateData } from '../services/membershipService';
import { useFeatureFlags } from '../contexts/FeatureFlagContext';
import SquarePaymentNew from './SquarePaymentNew';
import { londonTodayDateInput } from '../utils/timezone';
interface MembershipSetupProps {
onMembershipCreated: () => void;
onCancel: () => void;
}
const MembershipSetup: React.FC<MembershipSetupProps> = ({ onMembershipCreated, onCancel }) => {
const [tiers, setTiers] = useState<MembershipTier[]>([]);
const [selectedTier, setSelectedTier] = useState<MembershipTier | null>(null);
const [loading, setLoading] = useState(false);
const [step, setStep] = useState<'select' | 'payment' | 'confirm'>('select');
const [paymentMethod, setPaymentMethod] = useState<'square' | 'cash' | null>(null);
const [error, setError] = useState('');
const [createdMembershipId, setCreatedMembershipId] = useState<number | null>(null);
const { isEnabled } = useFeatureFlags();
useEffect(() => {
loadTiers();
}, []);
const loadTiers = async () => {
try {
const tierData = await membershipService.getTiers();
setTiers(tierData);
} catch (error) {
console.error('Failed to load tiers:', error);
setError('Failed to load membership tiers');
}
};
const handleTierSelect = (tier: MembershipTier) => {
setSelectedTier(tier);
setStep('payment');
};
const handleCashPayment = async () => {
if (!selectedTier || !createdMembershipId) return;
setLoading(true);
setError('');
try {
// Create cash/dummy payment
const paymentData: PaymentCreateData = {
amount: selectedTier.annual_fee,
payment_method: 'cash',
membership_id: createdMembershipId,
notes: `Cash payment for ${selectedTier.name} membership`
};
await paymentService.createPayment(paymentData);
setStep('confirm');
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to record payment');
} finally {
setLoading(false);
}
};
const handleSquarePaymentSuccess = (paymentResult: any) => {
console.log('Square payment successful:', paymentResult);
// Payment was successful, membership was created and activated by the backend
setStep('confirm');
};
const handleSquarePaymentError = (error: string) => {
setError(error);
setLoading(false);
};
const handlePaymentMethodSelect = async (method: 'square' | 'cash') => {
setPaymentMethod(method);
if (!selectedTier) return;
// For cash payments, create membership in PENDING state
// For Square payments, we'll create membership only after successful payment
if (method === 'cash') {
setLoading(true);
setError('');
try {
const startDate = londonTodayDateInput();
const endDateValue = new Date(`${startDate}T00:00:00Z`);
endDateValue.setUTCFullYear(endDateValue.getUTCFullYear() + 1);
const endDate = endDateValue.toISOString().split('T')[0];
const membershipData: MembershipCreateData = {
tier_id: selectedTier.id,
start_date: startDate,
end_date: endDate,
auto_renew: false
};
const membership = await membershipService.createMembership(membershipData);
setCreatedMembershipId(membership.id);
setLoading(false);
} catch (err: any) {
setError(err.response?.data?.detail || 'Failed to create membership');
setLoading(false);
}
}
// For Square, just set the payment method - membership created after successful payment
};
const handleConfirm = () => {
onMembershipCreated();
};
if (step === 'select') {
return (
<div className="card member-card member-membership-setup">
<div className="member-card-header">
<div>
<p className="member-card-kicker">Membership Setup</p>
<h3>Choose Your Membership</h3>
</div>
</div>
{error && <div className="alert alert-error">{error}</div>}
<div className="membership-tier-grid">
{tiers.map(tier => (
<div
key={tier.id}
className="membership-tier-card"
onClick={() => handleTierSelect(tier)}
>
<div className="membership-tier-header">
<h4>{tier.name}</h4>
<span className="membership-tier-price">
£{tier.annual_fee.toFixed(2)}/year
</span>
</div>
<p className="membership-tier-description">{tier.description}</p>
<div className="membership-tier-benefits">
<strong>Benefits:</strong>
<p>{tier.benefits}</p>
</div>
</div>
))}
</div>
<div className="membership-setup-actions">
<button
type="button"
className="btn btn-secondary"
onClick={onCancel}
>
Cancel
</button>
</div>
</div>
);
}
if (step === 'payment') {
return (
<div className="card member-card member-membership-setup">
<div className="member-card-header">
<div>
<p className="member-card-kicker">Membership Setup</p>
<h3>Complete Payment</h3>
</div>
</div>
{error && <div className="alert alert-error">{error}</div>}
{selectedTier && (
<div className="membership-summary-panel">
<h4>Selected Membership: {selectedTier.name}</h4>
<p><strong>Annual Fee:</strong> £{selectedTier.annual_fee.toFixed(2)}</p>
<p><strong>Benefits:</strong> {selectedTier.benefits}</p>
</div>
)}
{!paymentMethod && (
<div className="membership-payment-stage">
<h4 className="membership-payment-heading">Choose Payment Method</h4>
<div className="membership-payment-options">
<button
className="btn btn-primary"
onClick={() => handlePaymentMethodSelect('square')}
disabled={loading}
style={{ textAlign: 'left' }}
>
<div className="membership-payment-option-copy">
<strong>Credit/Debit Card</strong>
<div>
Pay securely with Square
</div>
</div>
<span></span>
</button>
{isEnabled('CASH_PAYMENT_ENABLED') && (
<button
className="btn btn-secondary"
onClick={() => handlePaymentMethodSelect('cash')}
disabled={loading}
style={{ textAlign: 'left' }}
>
<div className="membership-payment-option-copy">
<strong>Cash Payment</strong>
<div>
Pay in person or by check
</div>
</div>
<span></span>
</button>
)}
</div>
<div className="membership-setup-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => setStep('select')}
disabled={loading}
>
Back
</button>
</div>
</div>
)}
{paymentMethod === 'square' && selectedTier && (
<div>
<SquarePaymentNew
amount={selectedTier.annual_fee}
tierId={selectedTier.id}
onPaymentSuccess={handleSquarePaymentSuccess}
onPaymentError={handleSquarePaymentError}
/>
<div className="membership-setup-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setPaymentMethod(null);
setError('');
}}
disabled={loading}
>
Choose Different Payment Method
</button>
</div>
</div>
)}
{paymentMethod === 'cash' && createdMembershipId && (
<div>
<div className="membership-cash-notice">
<strong>Cash Payment Selected</strong>
<p>
Your membership will be marked as pending until an administrator confirms payment receipt.
</p>
</div>
<div className="membership-action-row">
<button
type="button"
className="btn btn-primary"
onClick={handleCashPayment}
disabled={loading}
>
{loading ? 'Processing...' : 'Confirm Cash Payment'}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setPaymentMethod(null);
setCreatedMembershipId(null);
setError('');
}}
disabled={loading}
>
Choose Different Payment Method
</button>
</div>
</div>
)}
</div>
);
}
if (step === 'confirm') {
const isCashPayment = paymentMethod === 'cash';
return (
<div className="card member-card member-membership-setup">
<div className="member-card-header">
<div>
<p className="member-card-kicker">Membership Setup</p>
<h3>
{isCashPayment ? 'Membership Application Submitted!' : 'Payment Successful!'}
</h3>
</div>
</div>
{selectedTier && (
<div className="membership-summary-panel">
<h4>Your Membership Details:</h4>
<p><strong>Tier:</strong> {selectedTier.name}</p>
<p><strong>Annual Fee:</strong> £{selectedTier.annual_fee.toFixed(2)}</p>
<p><strong>Status:</strong>
<span className={`status-badge ${isCashPayment ? 'status-pending' : 'status-active'}`}>
{isCashPayment ? 'Pending' : 'Active'}
</span>
</p>
<p className="membership-confirm-copy">
{isCashPayment
? 'Your membership application has been submitted. An administrator will review and activate your membership once payment is confirmed.'
: 'Thank you for your payment! Your membership has been activated and is now live. You can start enjoying your membership benefits immediately.'
}
</p>
</div>
)}
<div className="membership-setup-actions">
<button
type="button"
className="btn btn-primary"
onClick={handleConfirm}
>
Return to Dashboard
</button>
</div>
</div>
);
}
return null;
};
export default MembershipSetup;