1. New Components- src/components/PaymentMethodCard.js - Displays individual payment method- src/components/AddPaymentMethodDialog.js - Stripe Elements dialog for adding cards- src/components/PaymentMethodsSection.js - Main payment methods UI- src/components/PasswordConfirmDialog.js - Admin password re-entry dialog- src/components/admin/AdminPaymentMethodsPanel.js - Admin panel for user payment methods2. Profile Integration (src/pages/Profile.js)- Replaced placeholder Payment Method section with PaymentMethodsSection3. Admin Integration (src/pages/admin/AdminUserView.js)- Added AdminPaymentMethodsPanel to user detail view

This commit is contained in:
Koncept Kit
2026-01-31 01:09:37 +07:00
parent 7152382dca
commit 5d085153f6
7 changed files with 1412 additions and 15 deletions

View File

@@ -0,0 +1,186 @@
import React from 'react';
import { CreditCard, Trash2, Star, Banknote, Building2, FileCheck } from 'lucide-react';
import { Button } from './ui/button';
/**
* Card brand icon mapping
*/
const getBrandIcon = (brand) => {
const brandLower = brand?.toLowerCase();
// Return text abbreviation for known brands
switch (brandLower) {
case 'visa':
return 'VISA';
case 'mastercard':
return 'MC';
case 'amex':
case 'american_express':
return 'AMEX';
case 'discover':
return 'DISC';
default:
return null;
}
};
/**
* Get icon for payment method type
*/
const getPaymentTypeIcon = (paymentType) => {
switch (paymentType) {
case 'cash':
return Banknote;
case 'bank_transfer':
return Building2;
case 'check':
return FileCheck;
default:
return CreditCard;
}
};
/**
* Format payment type for display
*/
const formatPaymentType = (paymentType) => {
switch (paymentType) {
case 'cash':
return 'Cash';
case 'bank_transfer':
return 'Bank Transfer';
case 'check':
return 'Check';
case 'card':
return 'Card';
default:
return paymentType;
}
};
/**
* PaymentMethodCard - Displays a single payment method
*/
const PaymentMethodCard = ({
method,
onSetDefault,
onDelete,
loading = false,
showActions = true,
}) => {
const PaymentIcon = getPaymentTypeIcon(method.payment_type);
const brandAbbr = method.card_brand ? getBrandIcon(method.card_brand) : null;
const isExpired = method.card_exp_year && method.card_exp_month &&
new Date(method.card_exp_year, method.card_exp_month) < new Date();
return (
<div
className={`flex items-center justify-between p-4 border rounded-xl ${
method.is_default
? 'border-brand-purple bg-[var(--lavender-500)]'
: 'border-[var(--neutral-800)] bg-white'
} ${isExpired ? 'opacity-70' : ''}`}
>
<div className="flex items-center gap-4">
{/* Payment Method Icon */}
<div className={`p-3 rounded-full ${
method.is_default
? 'bg-brand-purple text-white'
: 'bg-[var(--lavender-300)] text-brand-purple'
}`}>
<PaymentIcon className="h-5 w-5" />
</div>
{/* Payment Method Details */}
<div>
{method.payment_type === 'card' ? (
<>
<div className="flex items-center gap-2">
{brandAbbr && (
<span className="text-xs font-bold text-[var(--purple-ink)] bg-[var(--lavender-300)] px-2 py-0.5 rounded">
{brandAbbr}
</span>
)}
<span
className="font-medium text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{method.card_brand ? method.card_brand.charAt(0).toUpperCase() + method.card_brand.slice(1) : 'Card'} {method.card_last4 || '****'}
</span>
{method.is_default && (
<span className="flex items-center gap-1 text-xs text-brand-purple font-medium">
<Star className="h-3 w-3 fill-current" />
Default
</span>
)}
</div>
<p
className={`text-sm ${isExpired ? 'text-red-500' : 'text-brand-purple'}`}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{isExpired ? 'Expired' : 'Expires'} {method.card_exp_month?.toString().padStart(2, '0')}/{method.card_exp_year?.toString().slice(-2)}
{method.card_funding && (
<span className="ml-2 text-xs capitalize">({method.card_funding})</span>
)}
</p>
</>
) : (
<>
<div className="flex items-center gap-2">
<span
className="font-medium text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPaymentType(method.payment_type)}
</span>
{method.is_default && (
<span className="flex items-center gap-1 text-xs text-brand-purple font-medium">
<Star className="h-3 w-3 fill-current" />
Default
</span>
)}
</div>
{method.manual_notes && (
<p
className="text-sm text-brand-purple"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{method.manual_notes}
</p>
)}
</>
)}
</div>
</div>
{/* Actions */}
{showActions && (
<div className="flex items-center gap-2">
{!method.is_default && (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onSetDefault?.(method.id)}
disabled={loading}
className="border border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-lg text-xs px-3"
>
Set Default
</Button>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onDelete?.(method.id)}
disabled={loading}
className="border border-red-500 text-red-500 hover:bg-red-50 rounded-lg p-2"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
};
export default PaymentMethodCard;