Add dues management system with UI improvements
Build And Push Image / docker (push) Successful in 3m3s
Details
Build And Push Image / docker (push) Successful in 3m3s
Details
- Add BoardDuesManagement and DuesActionCard components - Create API endpoints for dues status tracking and payment marking - Integrate dues management section into board dashboard - Move create portal account button to member card action buttons - Add edit button to member view dialog - Implement member update handlers and navigation between views
This commit is contained in:
parent
91dea9910d
commit
d3c3a865ba
|
|
@ -0,0 +1,221 @@
|
|||
<template>
|
||||
<v-card elevation="2" class="dues-management-card">
|
||||
<v-card-title class="pa-4 bg-warning-lighten-5">
|
||||
<v-icon class="mr-2" color="warning">mdi-cash-clock</v-icon>
|
||||
<span class="text-h6">Dues Management</span>
|
||||
<v-spacer />
|
||||
<v-chip color="warning" size="small">
|
||||
{{ overdueMembers.length + upcomingMembers.length }} Action Items
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-tabs v-model="activeTab" color="primary" class="mb-4">
|
||||
<v-tab value="overdue">
|
||||
<v-icon start>mdi-alert-circle</v-icon>
|
||||
Overdue ({{ overdueMembers.length }})
|
||||
</v-tab>
|
||||
<v-tab value="upcoming">
|
||||
<v-icon start>mdi-clock-alert</v-icon>
|
||||
Due Soon ({{ upcomingMembers.length }})
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window v-model="activeTab">
|
||||
<!-- Overdue Dues Tab -->
|
||||
<v-tabs-window-item value="overdue">
|
||||
<div v-if="overdueMembers.length === 0" class="text-center py-6">
|
||||
<v-icon size="48" color="success" class="mb-2">mdi-check-circle</v-icon>
|
||||
<p class="text-h6 text-success">All caught up!</p>
|
||||
<p class="text-body-2">No members have overdue dues.</p>
|
||||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="member in overdueMembers"
|
||||
:key="member.Id"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<DuesActionCard
|
||||
:member="member"
|
||||
status="overdue"
|
||||
@mark-paid="handleMarkPaid"
|
||||
@view-member="$emit('view-member', member)"
|
||||
:loading="loading[member.Id]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<!-- Upcoming Dues Tab -->
|
||||
<v-tabs-window-item value="upcoming">
|
||||
<div v-if="upcomingMembers.length === 0" class="text-center py-6">
|
||||
<v-icon size="48" color="info" class="mb-2">mdi-calendar-check</v-icon>
|
||||
<p class="text-h6 text-info">All up to date!</p>
|
||||
<p class="text-body-2">No upcoming dues in the next 30 days.</p>
|
||||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="member in upcomingMembers"
|
||||
:key="member.Id"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<DuesActionCard
|
||||
:member="member"
|
||||
status="upcoming"
|
||||
@mark-paid="handleMarkPaid"
|
||||
@view-member="$emit('view-member', member)"
|
||||
:loading="loading[member.Id]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:loading="refreshLoading"
|
||||
@click="refreshData"
|
||||
>
|
||||
<v-icon start>mdi-refresh</v-icon>
|
||||
Refresh
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="$emit('view-all-members')"
|
||||
>
|
||||
<v-icon start>mdi-account-group</v-icon>
|
||||
View All Members
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
interface Props {
|
||||
refreshTrigger?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'view-member', member: Member): void;
|
||||
(e: 'view-all-members'): void;
|
||||
(e: 'member-updated', member: Member): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// State
|
||||
const activeTab = ref('overdue');
|
||||
const overdueMembers = ref<Member[]>([]);
|
||||
const upcomingMembers = ref<Member[]>([]);
|
||||
const loading = ref<Record<string, boolean>>({});
|
||||
const refreshLoading = ref(false);
|
||||
|
||||
// Load dues data
|
||||
const loadDuesData = async () => {
|
||||
refreshLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await $fetch<{
|
||||
success: boolean;
|
||||
data: {
|
||||
overdue: Member[];
|
||||
upcoming: Member[];
|
||||
};
|
||||
}>('/api/members/dues-status');
|
||||
|
||||
if (response.success) {
|
||||
overdueMembers.value = response.data.overdue || [];
|
||||
upcomingMembers.value = response.data.upcoming || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dues data:', error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
refreshLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle mark as paid
|
||||
const handleMarkPaid = async (member: Member) => {
|
||||
loading.value[member.Id] = true;
|
||||
|
||||
try {
|
||||
const response = await $fetch<{
|
||||
success: boolean;
|
||||
data: Member;
|
||||
message?: string;
|
||||
}>(`/api/members/${member.Id}/mark-dues-paid`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Remove member from current lists
|
||||
overdueMembers.value = overdueMembers.value.filter(m => m.Id !== member.Id);
|
||||
upcomingMembers.value = upcomingMembers.value.filter(m => m.Id !== member.Id);
|
||||
|
||||
// Emit update event
|
||||
emit('member-updated', response.data);
|
||||
|
||||
// Show success message
|
||||
console.log('Dues marked as paid successfully');
|
||||
} else {
|
||||
throw new Error(response.message || 'Failed to mark dues as paid');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error marking dues as paid:', error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
loading.value[member.Id] = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Refresh data
|
||||
const refreshData = () => {
|
||||
loadDuesData();
|
||||
};
|
||||
|
||||
// Watch for refresh trigger
|
||||
watch(() => props.refreshTrigger, () => {
|
||||
if (props.refreshTrigger) {
|
||||
loadDuesData();
|
||||
}
|
||||
});
|
||||
|
||||
// Load data on mount
|
||||
onMounted(() => {
|
||||
loadDuesData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dues-management-card {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.bg-warning-lighten-5 {
|
||||
background-color: rgb(var(--v-theme-warning-lighten-5)) !important;
|
||||
}
|
||||
|
||||
.v-tab {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<v-card
|
||||
:class="[
|
||||
'dues-action-card',
|
||||
status === 'overdue' ? 'dues-action-card--overdue' : 'dues-action-card--upcoming'
|
||||
]"
|
||||
elevation="2"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
<div class="status-badge">
|
||||
<v-chip
|
||||
:color="statusColor"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
<v-icon start size="12">{{ statusIcon }}</v-icon>
|
||||
{{ statusText }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Member Info Header -->
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-avatar
|
||||
:color="avatarColor"
|
||||
size="40"
|
||||
class="mr-3"
|
||||
>
|
||||
<span class="text-white font-weight-bold">
|
||||
{{ memberInitials }}
|
||||
</span>
|
||||
</v-avatar>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="text-subtitle-1 font-weight-bold mb-1">
|
||||
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
|
||||
</h4>
|
||||
<div class="d-flex align-center">
|
||||
<v-chip size="x-small" color="grey" variant="text" class="pa-0 mr-2">
|
||||
ID: {{ member.member_id || `MUSA-${member.Id}` }}
|
||||
</v-chip>
|
||||
<CountryFlag
|
||||
v-if="member.nationality"
|
||||
:country-code="member.nationality.split(',')[0]"
|
||||
:show-name="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="contact-info mb-3">
|
||||
<div v-if="member.email" class="d-flex align-center mb-1">
|
||||
<v-icon size="14" class="mr-2 text-medium-emphasis">mdi-email</v-icon>
|
||||
<span class="text-body-2 text-truncate">{{ member.email }}</span>
|
||||
</div>
|
||||
<div v-if="member.phone" class="d-flex align-center">
|
||||
<v-icon size="14" class="mr-2 text-medium-emphasis">mdi-phone</v-icon>
|
||||
<span class="text-body-2">{{ member.FormattedPhone || member.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="$emit('mark-paid', member)"
|
||||
block
|
||||
>
|
||||
<v-icon start size="16">mdi-check-circle</v-icon>
|
||||
Mark as Paid
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="$emit('view-member', member)"
|
||||
>
|
||||
<v-icon start size="16">mdi-account</v-icon>
|
||||
View Details
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
:href="`mailto:${member.email}?subject=MonacoUSA Membership Dues Reminder`"
|
||||
target="_blank"
|
||||
v-if="member.email"
|
||||
>
|
||||
<v-icon start size="16">mdi-email</v-icon>
|
||||
Email
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
interface Props {
|
||||
member: Member;
|
||||
status: 'overdue' | 'upcoming';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'mark-paid', member: Member): void;
|
||||
(e: 'view-member', member: Member): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false
|
||||
});
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
// Computed properties
|
||||
const memberInitials = computed(() => {
|
||||
const firstName = props.member.first_name || '';
|
||||
const lastName = props.member.last_name || '';
|
||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||
});
|
||||
|
||||
const avatarColor = computed(() => {
|
||||
const colors = ['red', 'blue', 'green', 'orange', 'purple', 'teal', 'indigo', 'pink'];
|
||||
const idNumber = parseInt(props.member.Id) || 0;
|
||||
return colors[idNumber % colors.length];
|
||||
});
|
||||
|
||||
const statusColor = computed(() => {
|
||||
return props.status === 'overdue' ? 'error' : 'warning';
|
||||
});
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
return props.status === 'overdue' ? 'mdi-alert-circle' : 'mdi-clock-alert';
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
return props.status === 'overdue' ? 'Overdue' : 'Due Soon';
|
||||
});
|
||||
|
||||
const daysDifference = computed(() => {
|
||||
if (!props.member.payment_due_date) return null;
|
||||
|
||||
const today = new Date();
|
||||
const dueDate = new Date(props.member.payment_due_date);
|
||||
const diffTime = dueDate.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const formatDate = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dues-action-card {
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dues-action-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.dues-action-card--overdue {
|
||||
border-left: 4px solid rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.dues-action-card--upcoming {
|
||||
border-left: 4px solid rgb(var(--v-theme-warning));
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dues-info {
|
||||
background: rgba(var(--v-theme-surface-variant), 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.05);
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 600px) {
|
||||
.dues-action-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div v-if="canEdit || canDelete" class="member-action-buttons">
|
||||
<div v-if="canEdit || canDelete || (!member.keycloak_id && canCreatePortalAccount)" class="member-action-buttons">
|
||||
<v-btn
|
||||
v-if="canEdit"
|
||||
icon
|
||||
|
|
@ -43,6 +43,20 @@
|
|||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Create Portal Account Button (Circular) -->
|
||||
<v-btn
|
||||
v-if="!member.keycloak_id && canCreatePortalAccount"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
:loading="creatingPortalAccount"
|
||||
@click.stop="$emit('create-portal-account', member)"
|
||||
:title="'Create Portal Account for ' + member.FullName"
|
||||
>
|
||||
<v-icon>mdi-account-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
|
|
@ -170,20 +184,6 @@
|
|||
<v-icon start size="14">mdi-account-off</v-icon>
|
||||
No Portal Account
|
||||
</v-chip>
|
||||
|
||||
<!-- Create Portal Account Button -->
|
||||
<v-btn
|
||||
v-if="!member.keycloak_id && canCreatePortalAccount"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:loading="creatingPortalAccount"
|
||||
@click.stop="$emit('create-portal-account', member)"
|
||||
class="create-portal-btn"
|
||||
>
|
||||
<v-icon start size="14">mdi-account-plus</v-icon>
|
||||
Create Portal Account
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,14 @@
|
|||
>
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="$emit('edit', member)"
|
||||
>
|
||||
<v-icon start>mdi-pencil</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
@ -197,6 +205,7 @@ interface Props {
|
|||
|
||||
interface Emits {
|
||||
(e: 'update:model-value', value: boolean): void;
|
||||
(e: 'edit', member: Member): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,18 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Dues Management Section -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
<BoardDuesManagement
|
||||
:refresh-trigger="duesRefreshTrigger"
|
||||
@view-member="handleViewMember"
|
||||
@view-all-members="navigateToMembers"
|
||||
@member-updated="handleMemberUpdated"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Recent Board Activity -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
|
|
@ -224,6 +236,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'dashboard',
|
||||
middleware: 'auth'
|
||||
|
|
@ -241,6 +255,9 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
// Dues management state
|
||||
const duesRefreshTrigger = ref(0);
|
||||
|
||||
// Mock data for board dashboard
|
||||
const stats = ref({
|
||||
totalMembers: 156,
|
||||
|
|
@ -278,13 +295,32 @@ const recentActivity = ref([
|
|||
}
|
||||
]);
|
||||
|
||||
// Dues management handlers
|
||||
const handleViewMember = (member: Member) => {
|
||||
// Navigate to member details or open modal
|
||||
console.log('View member:', member.FullName || `${member.first_name} ${member.last_name}`);
|
||||
// You could implement member detail view here
|
||||
navigateToMembers();
|
||||
};
|
||||
|
||||
const handleMemberUpdated = (member: Member) => {
|
||||
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
|
||||
|
||||
// Trigger dues refresh to update the lists
|
||||
duesRefreshTrigger.value += 1;
|
||||
|
||||
// You could also update stats here if needed
|
||||
// stats.value = await fetchUpdatedStats();
|
||||
};
|
||||
|
||||
// Navigation methods (placeholder implementations)
|
||||
const navigateToMeetings = () => {
|
||||
console.log('Navigate to meetings');
|
||||
};
|
||||
|
||||
const navigateToMembers = () => {
|
||||
console.log('Navigate to members');
|
||||
// Navigate to member list page
|
||||
navigateTo('/dashboard/member-list');
|
||||
};
|
||||
|
||||
const navigateToReports = () => {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
<ViewMemberDialog
|
||||
v-model="showViewDialog"
|
||||
:member="selectedMember"
|
||||
@edit="editMember"
|
||||
/>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
|
|
@ -326,7 +327,9 @@ const filteredMembers = computed(() => {
|
|||
filtered = filtered.filter(member =>
|
||||
member.FullName?.toLowerCase().includes(search) ||
|
||||
member.email?.toLowerCase().includes(search) ||
|
||||
member.phone?.includes(search)
|
||||
member.phone?.includes(search) ||
|
||||
member.member_id?.toLowerCase().includes(search) ||
|
||||
`MUSA-${member.Id}`.toLowerCase().includes(search) // Search by generated member ID format
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
// server/api/members/[id]/mark-dues-paid.post.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const memberId = getRouterParam(event, 'id');
|
||||
|
||||
if (!memberId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
const { updateMember, getMemberById } = await import('~/server/utils/nocodb');
|
||||
|
||||
// Get current member data
|
||||
const currentMember = await getMemberById(memberId);
|
||||
|
||||
if (!currentMember) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const today = new Date();
|
||||
const updateData = {
|
||||
current_year_dues_paid: 'true',
|
||||
membership_date_paid: today.toISOString().split('T')[0], // YYYY-MM-DD format
|
||||
payment_due_date: undefined // Clear the due date since it's now paid
|
||||
};
|
||||
|
||||
// Update the member
|
||||
const updatedMember = await updateMember(memberId, updateData);
|
||||
|
||||
console.log(`[API] Successfully marked dues as paid for member ${memberId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedMember,
|
||||
message: `Dues marked as paid for ${updatedMember.first_name} ${updatedMember.last_name}`
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Error marking dues as paid:', error);
|
||||
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to mark dues as paid'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// server/api/members/dues-status.get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { getMembers } = await import('~/server/utils/nocodb');
|
||||
|
||||
// Get all members
|
||||
const allMembers = await getMembers();
|
||||
|
||||
if (!allMembers?.list) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
overdue: [],
|
||||
upcoming: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const thirtyDaysFromNow = new Date();
|
||||
thirtyDaysFromNow.setDate(today.getDate() + 30);
|
||||
|
||||
const overdueMembers: any[] = [];
|
||||
const upcomingMembers: any[] = [];
|
||||
|
||||
for (const member of allMembers.list) {
|
||||
// Skip if dues are already paid
|
||||
if (member.current_year_dues_paid === 'true') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if member has a payment due date
|
||||
if (member.payment_due_date) {
|
||||
const dueDate = new Date(member.payment_due_date);
|
||||
|
||||
if (dueDate < today) {
|
||||
// Overdue
|
||||
overdueMembers.push(member);
|
||||
} else if (dueDate <= thirtyDaysFromNow) {
|
||||
// Due within 30 days
|
||||
upcomingMembers.push(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by due date (most overdue/earliest due first)
|
||||
overdueMembers.sort((a, b) => {
|
||||
const dateA = new Date(a.payment_due_date);
|
||||
const dateB = new Date(b.payment_due_date);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
|
||||
upcomingMembers.sort((a, b) => {
|
||||
const dateA = new Date(a.payment_due_date);
|
||||
const dateB = new Date(b.payment_due_date);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
overdue: overdueMembers,
|
||||
upcoming: upcomingMembers
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Error fetching dues status:', error);
|
||||
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to fetch dues status'
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue