feat(loyalty): restructure program CRUD by interface
Some checks failed
Some checks failed
Move program CRUD from store to merchant/admin interfaces. Store becomes view-only for program config while merchant gets full CRUD and admin gets override capabilities. Merchant portal: - New API endpoints (GET/POST/PATCH/DELETE /program) - New settings page with create/edit/delete form - Overview page now has Create/Edit Program buttons - Settings menu item added to sidebar Admin portal: - New CRUD endpoints (create for merchant, update, delete) - New activate/deactivate program endpoints - Programs list has edit and toggle buttons per row - Merchant detail has create/delete/toggle program actions Store portal: - Removed POST/PATCH /program endpoints (now read-only) - Removed settings page route and template - Terminal, cards, stats, enroll unchanged Tests: 112 passed (58 new) covering merchant API, admin CRUD, store endpoint removal, and program service unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ function adminLoyaltyMerchantDetail() {
|
||||
// State
|
||||
loading: false,
|
||||
error: null,
|
||||
showDeleteModal: false,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
@@ -176,6 +177,67 @@ function adminLoyaltyMerchantDetail() {
|
||||
}
|
||||
},
|
||||
|
||||
// Create a default program for this merchant
|
||||
async createProgram() {
|
||||
try {
|
||||
const data = {
|
||||
loyalty_type: 'points',
|
||||
points_per_euro: 1,
|
||||
welcome_bonus_points: 0,
|
||||
minimum_redemption_points: 100,
|
||||
card_name: this.merchant?.name ? this.merchant.name + ' Loyalty' : 'Loyalty Program',
|
||||
is_active: true
|
||||
};
|
||||
const response = await apiClient.post(`/admin/loyalty/merchants/${this.merchantId}/program`, data);
|
||||
this.program = response;
|
||||
Utils.showToast('Loyalty program created', 'success');
|
||||
loyaltyMerchantDetailLog.info('Program created for merchant', this.merchantId);
|
||||
// Reload stats
|
||||
await this.loadStats();
|
||||
} catch (error) {
|
||||
Utils.showToast(`Failed to create program: ${error.message}`, 'error');
|
||||
loyaltyMerchantDetailLog.error('Failed to create program:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle program active/inactive
|
||||
async toggleActive() {
|
||||
if (!this.program) return;
|
||||
const action = this.program.is_active ? 'deactivate' : 'activate';
|
||||
try {
|
||||
const response = await apiClient.post(`/admin/loyalty/programs/${this.program.id}/${action}`);
|
||||
this.program.is_active = response.is_active;
|
||||
Utils.showToast(`Program ${action}d successfully`, 'success');
|
||||
loyaltyMerchantDetailLog.info(`Program ${action}d`);
|
||||
} catch (error) {
|
||||
Utils.showToast(`Failed to ${action} program: ${error.message}`, 'error');
|
||||
loyaltyMerchantDetailLog.error(`Failed to ${action} program:`, error);
|
||||
}
|
||||
},
|
||||
|
||||
// Show delete confirmation
|
||||
confirmDeleteProgram() {
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// Delete the program
|
||||
async deleteProgram() {
|
||||
if (!this.program) return;
|
||||
try {
|
||||
await apiClient.delete(`/admin/loyalty/programs/${this.program.id}`);
|
||||
this.program = null;
|
||||
this.showDeleteModal = false;
|
||||
Utils.showToast('Loyalty program deleted', 'success');
|
||||
loyaltyMerchantDetailLog.info('Program deleted');
|
||||
// Reload stats
|
||||
await this.loadStats();
|
||||
} catch (error) {
|
||||
Utils.showToast(`Failed to delete program: ${error.message}`, 'error');
|
||||
loyaltyMerchantDetailLog.error('Failed to delete program:', error);
|
||||
this.showDeleteModal = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format date for display
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
@@ -232,6 +232,20 @@ function adminLoyaltyPrograms() {
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle program active/inactive
|
||||
async toggleProgramActive(program) {
|
||||
const action = program.is_active ? 'deactivate' : 'activate';
|
||||
try {
|
||||
const response = await apiClient.post(`/admin/loyalty/programs/${program.id}/${action}`);
|
||||
program.is_active = response.is_active;
|
||||
Utils.showToast(`Program ${action}d successfully`, 'success');
|
||||
loyaltyProgramsLog.info(`Program ${program.id} ${action}d`);
|
||||
} catch (error) {
|
||||
Utils.showToast(`Failed to ${action} program: ${error.message}`, 'error');
|
||||
loyaltyProgramsLog.error(`Failed to ${action} program:`, error);
|
||||
}
|
||||
},
|
||||
|
||||
// Format date for display
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
Reference in New Issue
Block a user