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>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- 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-btn
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
icon
|
icon
|
||||||
|
|
@ -43,6 +43,20 @@
|
||||||
>
|
>
|
||||||
<v-icon>mdi-delete</v-icon>
|
<v-icon>mdi-delete</v-icon>
|
||||||
</v-btn>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Card Content -->
|
<!-- Card Content -->
|
||||||
|
|
@ -170,20 +184,6 @@
|
||||||
<v-icon start size="14">mdi-account-off</v-icon>
|
<v-icon start size="14">mdi-account-off</v-icon>
|
||||||
No Portal Account
|
No Portal Account
|
||||||
</v-chip>
|
</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>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,14 @@
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</v-btn>
|
</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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
@ -197,6 +205,7 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:model-value', value: boolean): void;
|
(e: 'update:model-value', value: boolean): void;
|
||||||
|
(e: 'edit', member: Member): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,18 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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 -->
|
<!-- Recent Board Activity -->
|
||||||
<v-row class="mb-6">
|
<v-row class="mb-6">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
|
|
@ -224,6 +236,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'dashboard',
|
layout: 'dashboard',
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
|
|
@ -241,6 +255,9 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dues management state
|
||||||
|
const duesRefreshTrigger = ref(0);
|
||||||
|
|
||||||
// Mock data for board dashboard
|
// Mock data for board dashboard
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
totalMembers: 156,
|
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)
|
// Navigation methods (placeholder implementations)
|
||||||
const navigateToMeetings = () => {
|
const navigateToMeetings = () => {
|
||||||
console.log('Navigate to meetings');
|
console.log('Navigate to meetings');
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToMembers = () => {
|
const navigateToMembers = () => {
|
||||||
console.log('Navigate to members');
|
// Navigate to member list page
|
||||||
|
navigateTo('/dashboard/member-list');
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToReports = () => {
|
const navigateToReports = () => {
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@
|
||||||
<ViewMemberDialog
|
<ViewMemberDialog
|
||||||
v-model="showViewDialog"
|
v-model="showViewDialog"
|
||||||
:member="selectedMember"
|
:member="selectedMember"
|
||||||
|
@edit="editMember"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
|
@ -326,7 +327,9 @@ const filteredMembers = computed(() => {
|
||||||
filtered = filtered.filter(member =>
|
filtered = filtered.filter(member =>
|
||||||
member.FullName?.toLowerCase().includes(search) ||
|
member.FullName?.toLowerCase().includes(search) ||
|
||||||
member.email?.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