Add payment date selection for dues payments and improve member viewing
Build And Push Image / docker (push) Successful in 3m23s
Details
Build And Push Image / docker (push) Successful in 3m23s
Details
- Add payment date dialog when marking dues as paid with date validation - Replace direct member view emission with dedicated ViewMemberDialog component - Add backend support for custom payment dates in mark-dues-paid endpoint - Prevent future date selection for payment records - Improve user workflow for viewing and editing member details
This commit is contained in:
parent
abf6ade8cd
commit
f1a462094a
|
|
@ -42,7 +42,7 @@
|
||||||
:member="member"
|
:member="member"
|
||||||
status="overdue"
|
status="overdue"
|
||||||
@mark-paid="handleMarkPaid"
|
@mark-paid="handleMarkPaid"
|
||||||
@view-member="$emit('view-member', member)"
|
@view-member="handleViewMember"
|
||||||
:loading="loading[member.Id]"
|
:loading="loading[member.Id]"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
:member="member"
|
:member="member"
|
||||||
status="upcoming"
|
status="upcoming"
|
||||||
@mark-paid="handleMarkPaid"
|
@mark-paid="handleMarkPaid"
|
||||||
@view-member="$emit('view-member', member)"
|
@view-member="handleViewMember"
|
||||||
:loading="loading[member.Id]"
|
:loading="loading[member.Id]"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
@ -99,6 +99,13 @@
|
||||||
View All Members
|
View All Members
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
|
<!-- View Member Dialog -->
|
||||||
|
<ViewMemberDialog
|
||||||
|
v-model="showViewDialog"
|
||||||
|
:member="selectedMember"
|
||||||
|
@edit="handleEditMember"
|
||||||
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -125,6 +132,10 @@ const upcomingMembers = ref<Member[]>([]);
|
||||||
const loading = ref<Record<string, boolean>>({});
|
const loading = ref<Record<string, boolean>>({});
|
||||||
const refreshLoading = ref(false);
|
const refreshLoading = ref(false);
|
||||||
|
|
||||||
|
// View member dialog state
|
||||||
|
const showViewDialog = ref(false);
|
||||||
|
const selectedMember = ref<Member | null>(null);
|
||||||
|
|
||||||
// Load dues data
|
// Load dues data
|
||||||
const loadDuesData = async () => {
|
const loadDuesData = async () => {
|
||||||
refreshLoading.value = true;
|
refreshLoading.value = true;
|
||||||
|
|
@ -184,6 +195,20 @@ const handleMarkPaid = async (member: Member) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle view member
|
||||||
|
const handleViewMember = (member: Member) => {
|
||||||
|
selectedMember.value = member;
|
||||||
|
showViewDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle edit member (from the view dialog)
|
||||||
|
const handleEditMember = (member: Member) => {
|
||||||
|
// Close the view dialog first
|
||||||
|
showViewDialog.value = false;
|
||||||
|
// Emit the view-member event which should trigger the edit dialog in the parent component
|
||||||
|
emit('view-member', member);
|
||||||
|
};
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
const refreshData = () => {
|
const refreshData = () => {
|
||||||
loadDuesData();
|
loadDuesData();
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
size="small"
|
size="small"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="$emit('mark-paid', member)"
|
@click="showPaymentDateDialog = true"
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
<v-icon start size="16">mdi-check-circle</v-icon>
|
<v-icon start size="16">mdi-check-circle</v-icon>
|
||||||
|
|
@ -133,6 +133,71 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
|
<!-- Payment Date Selection Dialog -->
|
||||||
|
<v-dialog v-model="showPaymentDateDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h6 pa-4">
|
||||||
|
<v-icon left color="success">mdi-calendar-check</v-icon>
|
||||||
|
Mark Dues as Paid
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-1 mb-2">
|
||||||
|
{{ member.FullName || `${member.first_name} ${member.last_name}` }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">
|
||||||
|
Select the date when the dues payment was received:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="selectedPaymentDate"
|
||||||
|
label="Payment Date"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
:max="todayDate"
|
||||||
|
prepend-inner-icon="mdi-calendar"
|
||||||
|
hint="Select the date when the payment was received"
|
||||||
|
persistent-hint
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="selectedPaymentDate && isDateInFuture"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-2"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-information</v-icon>
|
||||||
|
Future dates are not allowed. Please select today or an earlier date.
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4 pt-0">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="text"
|
||||||
|
@click="cancelPaymentDialog"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
variant="elevated"
|
||||||
|
:disabled="!selectedPaymentDate || isDateInFuture"
|
||||||
|
:loading="loading"
|
||||||
|
@click="confirmMarkAsPaid"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-check-circle</v-icon>
|
||||||
|
Confirm Payment
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<v-card-actions class="pa-4 pt-0">
|
<v-card-actions class="pa-4 pt-0">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -186,7 +251,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
// Reactive state for payment date dialog
|
||||||
|
const showPaymentDateDialog = ref(false);
|
||||||
|
const selectedPaymentDate = ref('');
|
||||||
|
|
||||||
|
// Initialize with today's date when dialog opens
|
||||||
|
watch(showPaymentDateDialog, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
selectedPaymentDate.value = todayDate.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const memberInitials = computed(() => {
|
const memberInitials = computed(() => {
|
||||||
|
|
@ -195,6 +271,21 @@ const memberInitials = computed(() => {
|
||||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const todayDate = computed(() => {
|
||||||
|
return new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDateInFuture = computed(() => {
|
||||||
|
if (!selectedPaymentDate.value) return false;
|
||||||
|
|
||||||
|
const selectedDate = new Date(selectedPaymentDate.value);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0); // Reset time to start of day
|
||||||
|
selectedDate.setHours(0, 0, 0, 0); // Reset time to start of day
|
||||||
|
|
||||||
|
return selectedDate > today;
|
||||||
|
});
|
||||||
|
|
||||||
const avatarColor = computed(() => {
|
const avatarColor = computed(() => {
|
||||||
const colors = ['red', 'blue', 'green', 'orange', 'purple', 'teal', 'indigo', 'pink'];
|
const colors = ['red', 'blue', 'green', 'orange', 'purple', 'teal', 'indigo', 'pink'];
|
||||||
const idNumber = parseInt(props.member.Id) || 0;
|
const idNumber = parseInt(props.member.Id) || 0;
|
||||||
|
|
@ -239,6 +330,41 @@ const formatDate = (dateString: string): string => {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelPaymentDialog = () => {
|
||||||
|
showPaymentDateDialog.value = false;
|
||||||
|
selectedPaymentDate.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmMarkAsPaid = async () => {
|
||||||
|
if (!selectedPaymentDate.value || isDateInFuture.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the API with the selected payment date
|
||||||
|
const response = await $fetch<{
|
||||||
|
success: boolean;
|
||||||
|
data: Member;
|
||||||
|
message?: string;
|
||||||
|
}>(`/api/members/${props.member.Id}/mark-dues-paid`, {
|
||||||
|
method: 'post',
|
||||||
|
body: {
|
||||||
|
paymentDate: selectedPaymentDate.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success && response.data) {
|
||||||
|
// Emit the mark-paid event with the updated member data
|
||||||
|
emit('mark-paid', response.data);
|
||||||
|
|
||||||
|
// Close the dialog and reset
|
||||||
|
showPaymentDateDialog.value = false;
|
||||||
|
selectedPaymentDate.value = '';
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error marking dues as paid:', error);
|
||||||
|
// You could show an error message here if needed
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get request body for custom payment date
|
||||||
|
const body = await readBody(event).catch(() => ({}));
|
||||||
|
const customPaymentDate = body.paymentDate;
|
||||||
|
|
||||||
const { updateMember, getMemberById } = await import('~/server/utils/nocodb');
|
const { updateMember, getMemberById } = await import('~/server/utils/nocodb');
|
||||||
|
|
||||||
// Get current member data
|
// Get current member data
|
||||||
|
|
@ -22,23 +26,44 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine payment date - use custom date if provided, otherwise today
|
||||||
|
let paymentDate: string;
|
||||||
|
if (customPaymentDate) {
|
||||||
|
try {
|
||||||
|
// Validate the custom date
|
||||||
|
const parsedDate = new Date(customPaymentDate);
|
||||||
|
if (isNaN(parsedDate.getTime())) {
|
||||||
|
throw new Error('Invalid date format');
|
||||||
|
}
|
||||||
|
paymentDate = parsedDate.toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid payment date provided. Please use YYYY-MM-DD format.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default to today if no custom date provided
|
||||||
|
paymentDate = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const today = new Date();
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
current_year_dues_paid: 'true',
|
current_year_dues_paid: 'true',
|
||||||
membership_date_paid: today.toISOString().split('T')[0], // YYYY-MM-DD format
|
membership_date_paid: paymentDate,
|
||||||
|
membership_status: 'Active', // Ensure member is marked as active when dues are paid
|
||||||
payment_due_date: undefined // Clear the due date since it's now paid
|
payment_due_date: undefined // Clear the due date since it's now paid
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the member
|
// Update the member
|
||||||
const updatedMember = await updateMember(memberId, updateData);
|
const updatedMember = await updateMember(memberId, updateData);
|
||||||
|
|
||||||
console.log(`[API] Successfully marked dues as paid for member ${memberId}`);
|
console.log(`[API] Successfully marked dues as paid for member ${memberId} with payment date: ${paymentDate}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: updatedMember,
|
data: updatedMember,
|
||||||
message: `Dues marked as paid for ${updatedMember.first_name} ${updatedMember.last_name}`
|
message: `Dues marked as paid for ${updatedMember.first_name} ${updatedMember.last_name} (Payment Date: ${paymentDate})`
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,15 @@ async function sanitizeMemberData(data: any): Promise<Partial<Member>> {
|
||||||
if (data.nationality) sanitized.nationality = data.nationality.trim();
|
if (data.nationality) sanitized.nationality = data.nationality.trim();
|
||||||
if (data.address) sanitized.address = data.address.trim();
|
if (data.address) sanitized.address = data.address.trim();
|
||||||
if (data.date_of_birth) sanitized.date_of_birth = data.date_of_birth;
|
if (data.date_of_birth) sanitized.date_of_birth = data.date_of_birth;
|
||||||
if (data.member_since) sanitized.member_since = data.member_since;
|
|
||||||
|
// Set member_since to provided date or default to today in YYYY-MM-DD format
|
||||||
|
if (data.member_since) {
|
||||||
|
sanitized.member_since = data.member_since;
|
||||||
|
} else {
|
||||||
|
sanitized.member_since = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||||
|
console.log('[api/members.post] Set member_since to current date:', sanitized.member_since);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.membership_date_paid) sanitized.membership_date_paid = data.membership_date_paid;
|
if (data.membership_date_paid) sanitized.membership_date_paid = data.membership_date_paid;
|
||||||
if (data.payment_due_date) sanitized.payment_due_date = data.payment_due_date;
|
if (data.payment_due_date) sanitized.payment_due_date = data.payment_due_date;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ export default defineEventHandler(async (event) => {
|
||||||
current_year_dues_paid: 'false',
|
current_year_dues_paid: 'false',
|
||||||
membership_status: 'Active',
|
membership_status: 'Active',
|
||||||
registration_date: new Date().toISOString(),
|
registration_date: new Date().toISOString(),
|
||||||
member_since: new Date().getFullYear().toString(),
|
member_since: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
|
||||||
membership_date_paid: '',
|
membership_date_paid: '',
|
||||||
payment_due_date: paymentDueDate.toISOString() // 1 month from registration
|
payment_due_date: paymentDueDate.toISOString() // 1 month from registration
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue