Add comprehensive dues tracking with overdue calculations and enhanced UI
All checks were successful
Build And Push Image / docker (push) Successful in 3m17s
All checks were successful
Build And Push Image / docker (push) Successful in 3m17s
This commit is contained in:
243
utils/dues-calculations.ts
Normal file
243
utils/dues-calculations.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import type { Member, DuesCalculationUtils } from '~/utils/types';
|
||||
|
||||
// Constants for date calculations
|
||||
export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
export const DAYS_IN_YEAR = 365;
|
||||
export const UPCOMING_THRESHOLD_DAYS = 30;
|
||||
|
||||
/**
|
||||
* Calculate days between two dates
|
||||
*/
|
||||
export function calculateDaysDifference(date1: Date, date2: Date): number {
|
||||
return Math.floor((date1.getTime() - date2.getTime()) / MILLISECONDS_PER_DAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a member is in their grace period
|
||||
* Used consistently across all dues components
|
||||
*/
|
||||
export function isInGracePeriod(member: Member): boolean {
|
||||
if (!member.payment_due_date) return false;
|
||||
|
||||
try {
|
||||
const dueDate = new Date(member.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
|
||||
* Used consistently across all dues components
|
||||
*/
|
||||
export function isPaymentOverOneYear(member: Member): boolean {
|
||||
if (!member.membership_date_paid) return false;
|
||||
|
||||
try {
|
||||
const lastPaidDate = new Date(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
|
||||
* Used consistently across all dues components
|
||||
* This is the main logic used throughout the system
|
||||
*/
|
||||
export function isDuesActuallyCurrent(member: Member): boolean {
|
||||
const paymentTooOld = isPaymentOverOneYear(member);
|
||||
const duesCurrentlyPaid = member.current_year_dues_paid === 'true';
|
||||
const gracePeriod = isInGracePeriod(member);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overdue days for a member
|
||||
*/
|
||||
export function calculateOverdueDays(member: Member): number {
|
||||
const today = new Date();
|
||||
const paymentTooOld = isPaymentOverOneYear(member);
|
||||
|
||||
if (paymentTooOld && member.membership_date_paid) {
|
||||
try {
|
||||
const lastPaidDate = new Date(member.membership_date_paid);
|
||||
return calculateDaysDifference(today, lastPaidDate) - DAYS_IN_YEAR;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (member.payment_due_date) {
|
||||
try {
|
||||
const dueDate = new Date(member.payment_due_date);
|
||||
return calculateDaysDifference(today, dueDate);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate days until due for upcoming members
|
||||
*/
|
||||
export function calculateDaysUntilDue(member: Member): { daysUntilDue: number; nextDueDate: string } | null {
|
||||
const today = new Date();
|
||||
|
||||
// First try membership_date_paid (existing members)
|
||||
if (member.membership_date_paid) {
|
||||
try {
|
||||
const lastPaidDate = new Date(member.membership_date_paid);
|
||||
const nextDueDate = new Date(lastPaidDate);
|
||||
nextDueDate.setFullYear(nextDueDate.getFullYear() + 1);
|
||||
|
||||
const daysUntilDue = calculateDaysDifference(nextDueDate, today);
|
||||
|
||||
if (daysUntilDue <= UPCOMING_THRESHOLD_DAYS && daysUntilDue > 0) {
|
||||
return {
|
||||
daysUntilDue,
|
||||
nextDueDate: nextDueDate.toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to payment_due_date check
|
||||
}
|
||||
}
|
||||
|
||||
// Try payment_due_date (new members in grace period)
|
||||
if (member.payment_due_date) {
|
||||
try {
|
||||
const dueDate = new Date(member.payment_due_date);
|
||||
const daysUntilDue = calculateDaysDifference(dueDate, today);
|
||||
|
||||
if (daysUntilDue <= UPCOMING_THRESHOLD_DAYS && daysUntilDue > 0) {
|
||||
return {
|
||||
daysUntilDue,
|
||||
nextDueDate: member.payment_due_date
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next dues date for a member
|
||||
* Used for display purposes
|
||||
*/
|
||||
export function getNextDuesDate(member: Member): string {
|
||||
// If dues are paid, calculate 1 year from payment date
|
||||
if (member.current_year_dues_paid === 'true' && member.membership_date_paid) {
|
||||
try {
|
||||
const lastPaidDate = new Date(member.membership_date_paid);
|
||||
const nextDue = new Date(lastPaidDate);
|
||||
nextDue.setFullYear(nextDue.getFullYear() + 1);
|
||||
return nextDue.toISOString().split('T')[0];
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
// If not paid but has a due date, use that
|
||||
if (member.payment_due_date) {
|
||||
return member.payment_due_date;
|
||||
}
|
||||
|
||||
// Fallback: 1 year from member since date
|
||||
if (member.member_since) {
|
||||
try {
|
||||
const memberSince = new Date(member.member_since);
|
||||
const nextDue = new Date(memberSince);
|
||||
nextDue.setFullYear(nextDue.getFullYear() + 1);
|
||||
return nextDue.toISOString().split('T')[0];
|
||||
} catch {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get days remaining until due date
|
||||
*/
|
||||
export function getDaysRemaining(member: Member): number {
|
||||
if (!member.payment_due_date) return 0;
|
||||
|
||||
try {
|
||||
const dueDate = new Date(member.payment_due_date);
|
||||
const today = new Date();
|
||||
const diffTime = dueDate.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / MILLISECONDS_PER_DAY);
|
||||
|
||||
return Math.max(0, diffDays);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable payment message for a member
|
||||
*/
|
||||
export function getPaymentMessage(member: Member, membershipFee: number = 50): string {
|
||||
const daysRemaining = getDaysRemaining(member);
|
||||
|
||||
if (daysRemaining > 30) {
|
||||
return `Your annual membership dues of €${membershipFee} are due in ${daysRemaining} days.`;
|
||||
} else if (daysRemaining > 0) {
|
||||
return `Your annual membership dues of €${membershipFee} are due in ${daysRemaining} days. Please pay soon to avoid account suspension.`;
|
||||
} else {
|
||||
return `Your annual membership dues of €${membershipFee} are overdue. Your account may be suspended soon.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all utilities as a single object
|
||||
* This matches the DuesCalculationUtils interface
|
||||
*/
|
||||
export const duesCalculationUtils: DuesCalculationUtils = {
|
||||
isInGracePeriod,
|
||||
isPaymentOverOneYear,
|
||||
isDuesActuallyCurrent,
|
||||
calculateOverdueDays,
|
||||
calculateDaysUntilDue
|
||||
};
|
||||
|
||||
/**
|
||||
* Default export for easy importing
|
||||
*/
|
||||
export default {
|
||||
// Core logic functions
|
||||
isInGracePeriod,
|
||||
isPaymentOverOneYear,
|
||||
isDuesActuallyCurrent,
|
||||
calculateOverdueDays,
|
||||
calculateDaysUntilDue,
|
||||
|
||||
// Utility functions
|
||||
calculateDaysDifference,
|
||||
getNextDuesDate,
|
||||
getDaysRemaining,
|
||||
getPaymentMessage,
|
||||
|
||||
// Constants
|
||||
MILLISECONDS_PER_DAY,
|
||||
DAYS_IN_YEAR,
|
||||
UPCOMING_THRESHOLD_DAYS
|
||||
};
|
||||
@@ -389,3 +389,46 @@ export interface AdminDashboardStats {
|
||||
[roleName: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Dues Management System Types
|
||||
export interface DuesMemberWithStatus extends Member {
|
||||
overdueDays?: number;
|
||||
overdueReason?: string;
|
||||
daysUntilDue?: number;
|
||||
nextDueDate?: string;
|
||||
}
|
||||
|
||||
export interface DuesStatusResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
overdue: DuesMemberWithStatus[];
|
||||
upcoming: DuesMemberWithStatus[];
|
||||
autoUpdated: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DuesOverdueCountResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
count: number;
|
||||
overdueMembers: DuesMemberWithStatus[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DuesPaymentInfo {
|
||||
membershipFee: number;
|
||||
iban: string;
|
||||
accountHolder: string;
|
||||
paymentReference: string;
|
||||
daysRemaining: number;
|
||||
paymentMessage: string;
|
||||
}
|
||||
|
||||
// Shared dues calculation utilities (used across components)
|
||||
export interface DuesCalculationUtils {
|
||||
isInGracePeriod: (member: Member) => boolean;
|
||||
isPaymentOverOneYear: (member: Member) => boolean;
|
||||
isDuesActuallyCurrent: (member: Member) => boolean;
|
||||
calculateOverdueDays: (member: Member) => number;
|
||||
calculateDaysUntilDue: (member: Member) => { daysUntilDue: number; nextDueDate: string } | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user