Add comprehensive dues tracking with overdue calculations and enhanced UI
All checks were successful
Build And Push Image / docker (push) Successful in 3m17s

This commit is contained in:
2025-08-11 15:29:42 +02:00
parent 7a8c88c341
commit abf6ade8cd
12 changed files with 658 additions and 130 deletions

View File

@@ -51,24 +51,57 @@
<!-- Dues Information -->
<div class="dues-info mb-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">mdi-calendar</v-icon>
{{ status === 'overdue' ? 'Was Due' : 'Due Date' }}
</span>
<span class="text-body-2 font-weight-bold" :class="status === 'overdue' ? 'text-error' : 'text-warning'">
{{ formatDate(member.payment_due_date) }}
</span>
<div v-if="status === 'overdue'">
<!-- Overdue Information -->
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">mdi-clock-alert</v-icon>
Days Overdue
</span>
<span class="text-body-2 font-weight-bold text-error">
{{ member.overdueDays || 0 }} days
</span>
</div>
<div v-if="member.overdueReason" class="overdue-reason">
<span class="text-caption text-error">
<v-icon size="12" class="mr-1">mdi-information</v-icon>
{{ member.overdueReason }}
</span>
</div>
<div v-if="member.membership_date_paid" class="d-flex justify-space-between align-center mt-2">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">mdi-calendar-check</v-icon>
Last Payment
</span>
<span class="text-body-2">
{{ formatDate(member.membership_date_paid) }}
</span>
</div>
</div>
<div v-if="daysDifference !== null" class="d-flex justify-space-between align-center">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">{{ status === 'overdue' ? 'mdi-clock-alert' : 'mdi-clock' }}</v-icon>
{{ status === 'overdue' ? 'Days Overdue' : 'Days Until Due' }}
</span>
<span class="text-body-2 font-weight-bold" :class="status === 'overdue' ? 'text-error' : 'text-warning'">
{{ Math.abs(daysDifference) }} days
</span>
<div v-else>
<!-- Upcoming Information -->
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">mdi-calendar</v-icon>
Due Date
</span>
<span class="text-body-2 font-weight-bold text-warning">
{{ formatDate(member.nextDueDate || member.payment_due_date) }}
</span>
</div>
<div class="d-flex justify-space-between align-center">
<span class="text-body-2 text-medium-emphasis">
<v-icon size="14" class="mr-1">mdi-clock</v-icon>
Days Until Due
</span>
<span class="text-body-2 font-weight-bold text-warning">
{{ member.daysUntilDue || 0 }} days
</span>
</div>
</div>
</div>
@@ -130,8 +163,16 @@
<script setup lang="ts">
import type { Member } from '~/utils/types';
// Extended member type for dues management
interface DuesMember extends Member {
overdueDays?: number;
overdueReason?: string;
daysUntilDue?: number;
nextDueDate?: string;
}
interface Props {
member: Member;
member: DuesMember;
status: 'overdue' | 'upcoming';
loading?: boolean;
}

View File

@@ -173,13 +173,65 @@ const snackbar = ref({
color: 'success'
});
/**
* 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;
}
});
/**
* Check if dues are actually current
* Uses the same logic as dues-status API and MemberCard
*/
const isDuesActuallyCurrent = computed(() => {
if (!memberData.value) return false;
const paymentTooOld = isPaymentOverOneYear.value;
const duesCurrentlyPaid = memberData.value.current_year_dues_paid === 'true';
const gracePeriod = isInGracePeriod.value;
// Member is NOT overdue if they're in grace period OR (dues paid AND payment not too old)
const isOverdue = paymentTooOld || (!duesCurrentlyPaid && !gracePeriod);
return !isOverdue;
});
// Computed properties
const shouldShowBanner = computed(() => {
if (!user.value || !memberData.value) return false;
if (dismissed.value) return false;
// Show banner if member exists and has unpaid dues
return memberData.value.current_year_dues_paid === 'false';
// Show banner if dues are NOT current
return !isDuesActuallyCurrent.value;
});
const daysRemaining = computed(() => {

View File

@@ -261,39 +261,54 @@ const statusColor = computed(() => {
}
});
// Helper to check if dues are actually current (paid within last 12 months)
const isDuesActuallyCurrent = computed(() => {
if (props.member.current_year_dues_paid !== 'true') return false;
/**
* Check if a member is in their grace period
* Uses the same logic as dues-status API
*/
const isInGracePeriod = computed(() => {
if (!props.member.payment_due_date) return false;
if (!props.member.membership_date_paid) {
// If marked as paid but no payment date, consider it invalid/overdue
try {
const dueDate = new Date(props.member.payment_due_date);
const today = new Date();
return dueDate > today;
} catch {
return false;
}
const paymentDate = new Date(props.member.membership_date_paid);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
return paymentDate > oneYearAgo;
});
// Helper to check if member is in grace period (new members get 1 month)
const isInGracePeriod = computed(() => {
// For existing members, check if they have member_since and it's within 1 month
if (props.member.member_since) {
const memberSince = new Date(props.member.member_since);
const oneMonthLater = new Date(memberSince);
oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);
return new Date() < oneMonthLater && props.member.current_year_dues_paid !== 'true';
}
/**
* Check if a member's last payment is over 1 year old
* Uses the same logic as dues-status API
*/
const isPaymentOverOneYear = computed(() => {
if (!props.member.membership_date_paid) return false;
// If no member_since but has payment_due_date in the future, assume in grace period
if (props.member.payment_due_date) {
const dueDate = new Date(props.member.payment_due_date);
return new Date() < dueDate && props.member.current_year_dues_paid !== 'true';
try {
const lastPaidDate = new Date(props.member.membership_date_paid);
const oneYearFromPayment = new Date(lastPaidDate);
oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1);
const today = new Date();
return today > oneYearFromPayment;
} catch {
return false;
}
});
/**
* Check if dues are actually current
* Uses the same logic as dues-status API
*/
const isDuesActuallyCurrent = computed(() => {
const paymentTooOld = isPaymentOverOneYear.value;
const duesCurrentlyPaid = props.member.current_year_dues_paid === 'true';
const gracePeriod = isInGracePeriod.value;
return false;
// Member is NOT overdue if they're in grace period OR (dues paid AND payment not too old)
const isOverdue = paymentTooOld || (!duesCurrentlyPaid && !gracePeriod);
return !isOverdue;
});
const duesColor = computed(() => {