forked from jamesp/sasa-membership
d024bf7fa3
- 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
343 lines
11 KiB
TypeScript
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;
|