2025-08-08 19:40:13 +02:00
< template >
< v -banner
v - if = "showBanner"
color = "warning"
icon = "mdi-alert-circle"
sticky
class = "dues-payment-banner"
>
< template # text >
< div class = "banner-content" >
< div class = "text-h6 font-weight-bold mb-2" >
< v -icon left > mdi - credit - card - alert < / v - i c o n >
Membership Dues Payment Required
< / div >
< div class = "text-body-1 mb-3" >
{ { paymentMessage } }
< / div >
< v -card
class = "payment-details-card pa-3"
color = "rgba(255,255,255,0.1)"
variant = "outlined"
>
< div class = "text-subtitle-1 font-weight-bold mb-2" >
< v -icon left size = "small" > mdi - bank < / v - i c o n >
Payment Details
< / div >
< v -row dense >
< v -col cols = "12" sm = "4" md = "3" >
< div class = "text-caption font-weight-bold" > Amount : < / div >
< div class = "text-body-2" > € { { config . membershipFee } } / year < / div >
< / v - c o l >
< v -col cols = "12" sm = "8" md = "5" v-if ="config.iban" >
< div class = "text-caption font-weight-bold" > IBAN : < / div >
< div class = "text-body-2 font-family-monospace" > { { config . iban } } < / div >
< / v - c o l >
< v -col cols = "12" sm = "12" md = "4" v-if ="config.accountHolder" >
< div class = "text-caption font-weight-bold" > Account Holder : < / div >
< div class = "text-body-2" > { { config . accountHolder } } < / div >
< / v - c o l >
< / v - r o w >
2025-08-10 16:49:23 +02:00
< v -divider class = "my-2" / >
< v -row dense >
< v -col cols = "12" >
< div class = "text-caption font-weight-bold" > Payment Reference : < / div >
< div class = "text-body-2 font-family-monospace" style = "background-color: rgba(163, 21, 21, 0.1); padding: 8px; border-radius: 4px; border-left: 4px solid #a31515;" >
{ { memberData ? . member _id || 'Member ID pending' } }
< / div >
< div class = "text-caption text-medium-emphasis mt-1" >
< v -icon size = "small" class = "mr-1" > mdi - information - outline < / v - i c o n >
Please include your member ID in the wire transfer reference for identification
< / div >
< / v - c o l >
< / v - r o w >
2025-08-08 19:40:13 +02:00
< v -divider class = "my-2" / >
< div class = "text-caption d-flex align-center" >
< v -icon size = "small" class = "mr-1" > mdi - information - outline < / v - i c o n >
{ { daysRemaining > 0 ? ` ${ daysRemaining } days remaining ` : 'Payment overdue' } }
before account suspension
< / div >
< / v - c a r d >
< / div >
< / template >
< template # actions >
< v -btn
v - if = "isAdmin"
color = "white"
variant = "outlined"
size = "small"
@ click = "markAsPaidDialog = true"
class = "mr-2"
>
< v -icon left size = "small" > mdi - check - circle < / v - i c o n >
Mark as Paid
< / v - b t n >
< v -btn
color = "white"
variant = "text"
size = "small"
@ click = "dismissBanner"
>
< v -icon left size = "small" > mdi - close < / v - i c o n >
Dismiss
< / v - b t n >
< / template >
< / v - b a n n e r >
<!-- Mark as Paid Dialog -- >
< v -dialog v-model ="markAsPaidDialog" max-width="400" >
< v -card >
< v -card -title class = "text-h6" >
< v -icon left color = "success" > mdi - check - circle < / v - i c o n >
Mark Dues as Paid
< / v - c a r d - t i t l e >
< v -card -text >
< p > Are you sure you want to mark the dues as paid for this member ? < / p >
< p class = "text-body-2 text-medium-emphasis" >
This will remove the payment banner and update the member ' s status .
< / p >
< / v - c a r d - t e x t >
< v -card -actions >
< v -spacer / >
< v -btn
color = "grey"
variant = "text"
@ click = "markAsPaidDialog = false"
>
Cancel
< / v - b t n >
< v -btn
color = "success"
variant = "flat"
: loading = "updating"
@ click = "markDuesAsPaid"
>
Mark as Paid
< / v - b t n >
< / v - c a r d - a c t i o n s >
< / v - c a r d >
< / v - d i a l o g >
<!-- Snackbar for notifications -- >
< v -snackbar
v - model = "snackbar.show"
: color = "snackbar.color"
: timeout = "4000"
>
{ { snackbar . message } }
< template # actions >
< v -btn
variant = "text"
@ click = "snackbar.show = false"
>
Close
< / v - b t n >
< / template >
< / v - s n a c k b a r >
< / template >
< script setup lang = "ts" >
import type { RegistrationConfig , Member } from '~/utils/types' ;
// Get auth state
const { user , isAdmin } = useAuth ( ) ;
// Reactive state
const showBanner = ref ( false ) ;
const dismissed = ref ( false ) ;
const markAsPaidDialog = ref ( false ) ;
const updating = ref ( false ) ;
const memberData = ref < Member | null > ( null ) ;
const config = ref < RegistrationConfig > ( {
membershipFee : 50 ,
iban : '' ,
accountHolder : ''
} ) ;
const snackbar = ref ( {
show : false ,
message : '' ,
color : 'success'
} ) ;
2025-08-11 15:29:42 +02:00
/ * *
* Check if a member is in their grace period
* Uses the same logic as dues - status API
* /
const isInGracePeriod = computed ( ( ) => {
if ( ! memberData . value ? . payment _due _date ) return false ;
try {
const dueDate = new Date ( memberData . value . payment _due _date ) ;
const today = new Date ( ) ;
return dueDate > today ;
} catch {
return false ;
}
} ) ;
/ * *
* Check if a member ' s last payment is over 1 year old
* Uses the same logic as dues - status API
* /
const isPaymentOverOneYear = computed ( ( ) => {
if ( ! memberData . value ? . membership _date _paid ) return false ;
try {
const lastPaidDate = new Date ( memberData . value . membership _date _paid ) ;
const oneYearFromPayment = new Date ( lastPaidDate ) ;
oneYearFromPayment . setFullYear ( oneYearFromPayment . getFullYear ( ) + 1 ) ;
const today = new Date ( ) ;
return today > oneYearFromPayment ;
} catch {
return false ;
}
} ) ;
/ * *
2025-08-12 04:25:35 +02:00
* Check if dues need to be paid ( either overdue or in grace period )
* Banner should show when payment is needed
2025-08-11 15:29:42 +02:00
* /
2025-08-12 04:25:35 +02:00
const needsPayment = computed ( ( ) => {
2025-08-11 15:29:42 +02:00
if ( ! memberData . value ) return false ;
const duesCurrentlyPaid = memberData . value . current _year _dues _paid === 'true' ;
2025-08-12 04:25:35 +02:00
const paymentTooOld = isPaymentOverOneYear . value ;
2025-08-11 15:29:42 +02:00
2025-08-12 04:25:35 +02:00
// Show banner if:
// 1. Dues are not currently paid (regardless of grace period)
// 2. OR payment is over 1 year old (even if marked as paid)
return ! duesCurrentlyPaid || paymentTooOld ;
2025-08-11 15:29:42 +02:00
} ) ;
2025-08-08 19:40:13 +02:00
// Computed properties
const shouldShowBanner = computed ( ( ) => {
if ( ! user . value || ! memberData . value ) return false ;
if ( dismissed . value ) return false ;
2025-08-12 04:25:35 +02:00
// Show banner when payment is needed
return needsPayment . value ;
2025-08-08 19:40:13 +02:00
} ) ;
const daysRemaining = computed ( ( ) => {
if ( ! memberData . value ? . payment _due _date ) return 0 ;
const dueDate = new Date ( memberData . value . payment _due _date ) ;
const today = new Date ( ) ;
const diffTime = dueDate . getTime ( ) - today . getTime ( ) ;
const diffDays = Math . ceil ( diffTime / ( 1000 * 60 * 60 * 24 ) ) ;
return Math . max ( 0 , diffDays ) ;
} ) ;
const paymentMessage = computed ( ( ) => {
if ( daysRemaining . value > 30 ) {
return ` Your annual membership dues of € ${ config . value . membershipFee } are due in ${ daysRemaining . value } days. ` ;
} else if ( daysRemaining . value > 0 ) {
return ` Your annual membership dues of € ${ config . value . membershipFee } are due in ${ daysRemaining . value } days. Please pay soon to avoid account suspension. ` ;
} else {
return ` Your annual membership dues of € ${ config . value . membershipFee } are overdue. Your account may be suspended soon. ` ;
}
} ) ;
// Methods
function dismissBanner ( ) {
dismissed . value = true ;
showBanner . value = false ;
// Store dismissal in localStorage (expires after 24 hours)
const dismissalData = {
timestamp : Date . now ( ) ,
userId : user . value ? . id
} ;
localStorage . setItem ( 'dues-banner-dismissed' , JSON . stringify ( dismissalData ) ) ;
}
async function markDuesAsPaid ( ) {
if ( ! memberData . value ? . Id ) return ;
updating . value = true ;
try {
// Update member's dues status
await $fetch ( ` /api/members/ ${ memberData . value . Id } ` , {
method : 'PUT' ,
body : {
current _year _dues _paid : 'true' ,
membership _date _paid : new Date ( ) . toISOString ( ) ,
payment _due _date : new Date ( Date . now ( ) + 365 * 24 * 60 * 60 * 1000 ) . toISOString ( ) // Next year
}
} ) ;
// Update local member state
if ( memberData . value ) {
memberData . value . current _year _dues _paid = 'true' ;
memberData . value . membership _date _paid = new Date ( ) . toISOString ( ) ;
}
// Hide banner
showBanner . value = false ;
markAsPaidDialog . value = false ;
// Show success message
snackbar . value = {
show : true ,
message : 'Dues marked as paid successfully!' ,
color : 'success'
} ;
} catch ( error : any ) {
console . error ( 'Failed to mark dues as paid:' , error ) ;
snackbar . value = {
show : true ,
message : 'Failed to update payment status. Please try again.' ,
color : 'error'
} ;
} finally {
updating . value = false ;
}
}
// Load member data for the current user
async function loadMemberData ( ) {
if ( ! user . value ? . email ) return ;
try {
const response = await $fetch ( '/api/members' ) as any ;
const members = response ? . data || response ? . list || [ ] ;
// Find member by email
const member = members . find ( ( m : any ) => m . email === user . value ? . email ) ;
if ( member ) {
memberData . value = member ;
}
} catch ( error ) {
console . warn ( 'Failed to load member data:' , error ) ;
}
}
// Load configuration and check banner visibility
async function loadConfig ( ) {
try {
2025-08-12 04:25:35 +02:00
const response = await $fetch ( '/api/registration-config' ) as any ;
2025-08-08 19:40:13 +02:00
if ( response ? . success ) {
config . value = response . data ;
}
} catch ( error ) {
console . warn ( 'Failed to load registration config:' , error ) ;
}
}
// Check if banner was recently dismissed
function checkDismissalStatus ( ) {
try {
const stored = localStorage . getItem ( 'dues-banner-dismissed' ) ;
if ( stored ) {
const dismissalData = JSON . parse ( stored ) ;
const hoursSinceDismissal = ( Date . now ( ) - dismissalData . timestamp ) / ( 1000 * 60 * 60 ) ;
// Reset dismissal after 24 hours or if different user
if ( hoursSinceDismissal > 24 || dismissalData . userId !== user . value ? . id ) {
localStorage . removeItem ( 'dues-banner-dismissed' ) ;
dismissed . value = false ;
} else {
dismissed . value = true ;
}
}
} catch ( error ) {
console . warn ( 'Failed to check dismissal status:' , error ) ;
dismissed . value = false ;
}
}
// Watchers
watch ( shouldShowBanner , ( newVal ) => {
showBanner . value = newVal ;
} , { immediate : true } ) ;
watch ( user , ( ) => {
checkDismissalStatus ( ) ;
loadMemberData ( ) ;
} , { immediate : true } ) ;
// Initialize
onMounted ( ( ) => {
loadConfig ( ) ;
checkDismissalStatus ( ) ;
loadMemberData ( ) ;
} ) ;
< / script >
< style scoped >
. dues - payment - banner {
border - left : 4 px solid # ff9800 ;
}
. banner - content {
width : 100 % ;
}
. payment - details - card {
backdrop - filter : blur ( 10 px ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ! important ;
}
/* Mobile responsiveness */
@ media ( max - width : 600 px ) {
. banner - content . text - h6 {
font - size : 1.1 rem ! important ;
}
. payment - details - card {
margin - top : 8 px ;
}
}
< / style >