2025-08-08 19:40:13 +02:00
< template >
< v -banner
v - if = "showBanner"
2025-08-15 14:48:19 +02:00
: color = "isOverdue ? 'error' : 'warning'"
: icon = "isOverdue ? 'mdi-alert-octagon' : 'mdi-alert-circle'"
2025-08-08 19:40:13 +02:00
sticky
2025-08-15 14:48:19 +02:00
: class = "['dues-payment-banner', { 'overdue-banner': isOverdue }]"
2025-08-08 19:40:13 +02:00
>
< template # text >
< div class = "banner-content" >
< div class = "text-h6 font-weight-bold mb-2" >
2025-08-15 14:48:19 +02:00
< v -icon left > { { isOverdue ? 'mdi-alert-octagon' : 'mdi-credit-card-alert' } } < / v - i c o n >
{ { isOverdue ? '🚨 URGENT: Overdue Dues Payment' : 'Membership Dues Payment Required' } }
2025-08-08 19:40:13 +02:00
< / 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-15 15:02:56 +02:00
* Calculate next dues date ( 1 year from when they last paid or joined )
2025-08-15 14:48:19 +02:00
* /
2025-08-15 15:02:56 +02:00
const nextDuesDate = computed ( ( ) => {
if ( ! memberData . value ) return null ;
2025-08-15 14:48:19 +02:00
2025-08-15 15:02:56 +02:00
// If dues are paid, calculate 1 year from payment date
if ( memberData . value . current _year _dues _paid === 'true' && memberData . value . membership _date _paid ) {
const lastPaidDate = new Date ( memberData . value . membership _date _paid ) ;
const nextDue = new Date ( lastPaidDate ) ;
nextDue . setFullYear ( nextDue . getFullYear ( ) + 1 ) ;
return nextDue ;
2025-08-15 14:48:19 +02:00
}
2025-08-15 15:02:56 +02:00
// If not paid but has a due date, use that
if ( memberData . value . payment _due _date ) {
return new Date ( memberData . value . payment _due _date ) ;
}
// Fallback: 1 year from member since date
if ( memberData . value . member _since ) {
const memberSince = new Date ( memberData . value . member _since ) ;
const nextDue = new Date ( memberSince ) ;
nextDue . setFullYear ( nextDue . getFullYear ( ) + 1 ) ;
return nextDue ;
}
return null ;
2025-08-15 14:48:19 +02:00
} ) ;
/ * *
2025-08-15 15:02:56 +02:00
* Check if dues are coming due within 30 days ( for paid members )
2025-08-11 15:29:42 +02:00
* /
2025-08-15 15:02:56 +02:00
const isDueSoon = computed ( ( ) => {
if ( ! memberData . value || ! nextDuesDate . value ) return false ;
// Only show warning if dues are currently paid
if ( memberData . value . current _year _dues _paid !== 'true' ) return false ;
const today = new Date ( ) ;
const thirtyDaysFromNow = new Date ( ) ;
thirtyDaysFromNow . setDate ( thirtyDaysFromNow . getDate ( ) + 30 ) ;
// Show banner if due date is within the next 30 days
return nextDuesDate . value <= thirtyDaysFromNow && nextDuesDate . value > today ;
} ) ;
/ * *
* Check if dues are overdue
* /
const isDuesOverdue = computed ( ( ) => {
2025-08-11 15:29:42 +02:00
if ( ! memberData . value ) return false ;
2025-08-15 15:02:56 +02:00
// If dues are current, not overdue
2025-08-11 15:29:42 +02:00
const duesCurrentlyPaid = memberData . value . current _year _dues _paid === 'true' ;
2025-08-12 04:25:35 +02:00
const paymentTooOld = isPaymentOverOneYear . value ;
2025-08-15 15:02:56 +02:00
const gracePeriod = isInGracePeriod . value ;
2025-08-11 15:29:42 +02:00
2025-08-15 15:02:56 +02:00
// Member is overdue if payment is too old OR (dues not paid AND not in grace period)
return paymentTooOld || ( ! duesCurrentlyPaid && ! gracePeriod ) ;
} ) ;
/ * *
* Check if dues need to be paid ( either coming due soon or overdue )
* /
const needsPayment = computed ( ( ) => {
if ( ! memberData . value ) return false ;
// Show banner if dues are coming due soon OR overdue
return isDueSoon . value || isDuesOverdue . value ;
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 ( ( ) => {
2025-08-15 15:02:56 +02:00
if ( ! nextDuesDate . value ) return 0 ;
2025-08-08 19:40:13 +02:00
2025-08-15 15:02:56 +02:00
const dueDate = nextDuesDate . value ;
2025-08-08 19:40:13 +02:00
const today = new Date ( ) ;
const diffTime = dueDate . getTime ( ) - today . getTime ( ) ;
const diffDays = Math . ceil ( diffTime / ( 1000 * 60 * 60 * 24 ) ) ;
2025-08-15 14:48:19 +02:00
return diffDays ; // Allow negative values for overdue
} ) ;
const isOverdue = computed ( ( ) => {
2025-08-15 15:02:56 +02:00
return isDuesOverdue . value ;
2025-08-08 19:40:13 +02:00
} ) ;
const paymentMessage = computed ( ( ) => {
2025-08-15 15:02:56 +02:00
if ( isDuesOverdue . value ) {
2025-08-15 14:48:19 +02:00
const overdueDays = Math . abs ( daysRemaining . value ) ;
2025-08-15 15:02:56 +02:00
return ` Your annual membership dues of € ${ config . value . membershipFee } are ${ overdueDays > 0 ? overdueDays + ' day' + ( overdueDays !== 1 ? 's' : '' ) + ' ' : '' } overdue. Immediate payment is required to avoid account suspension. ` ;
} else if ( isDueSoon . value ) {
const dueDays = daysRemaining . value ;
if ( dueDays <= 7 ) {
return ` Your annual membership dues of € ${ config . value . membershipFee } are due in ${ dueDays } day ${ dueDays !== 1 ? 's' : '' } . Please pay immediately to avoid late fees. ` ;
} else {
return ` Your annual membership dues of € ${ config . value . membershipFee } are due in ${ dueDays } day ${ dueDays !== 1 ? 's' : '' } . Please pay soon to avoid account suspension. ` ;
}
2025-08-08 19:40:13 +02:00
} else {
2025-08-15 15:02:56 +02:00
return ` Your annual membership dues of € ${ config . value . membershipFee } require attention. ` ;
2025-08-08 19:40:13 +02:00
}
} ) ;
// 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 ;
}
}
2025-08-15 15:02:56 +02:00
// Load member data for the current user from session
2025-08-08 19:40:13 +02:00
async function loadMemberData ( ) {
2025-08-15 15:02:56 +02:00
if ( ! user . value ) return ;
2025-08-08 19:40:13 +02:00
try {
2025-08-15 15:02:56 +02:00
const response = await $fetch ( '/api/auth/session' ) as any ;
if ( response ? . success && response ? . member ) {
memberData . value = response . member ;
2025-08-08 19:40:13 +02:00
}
} 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 ;
}
2025-08-15 14:48:19 +02:00
. dues - payment - banner . overdue - banner {
border - left : 4 px solid # f44336 ;
animation : pulse - border 2 s infinite ;
}
@ keyframes pulse - border {
0 % { border - left - color : # f44336 ; }
50 % { border - left - color : # ff5252 ; }
100 % { border - left - color : # f44336 ; }
}
2025-08-08 19:40:13 +02:00
. 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 >