feat(loyalty): add dedicated program edit page with full CRUD and tests
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

Add /admin/loyalty/merchants/{id}/program route for program configuration
with a dedicated Alpine.js edit page supporting create/edit/delete flows.
Restructure programs dashboard with create modal (merchant search +
duplicate detection) and delete confirmation. Rename "Loyalty Settings"
to "Admin Policy" for clearer separation of concerns.

Add integration tests for all admin page routes (12 tests) and program
list search/filter/pagination endpoints (9 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 23:25:22 +01:00
parent 6b46a78e72
commit f1e7baaa6c
9 changed files with 1056 additions and 13 deletions

View File

@@ -29,6 +29,17 @@ function adminLoyaltyPrograms() {
loading: false,
error: null,
// Delete modal state
showDeleteModal: false,
deletingProgram: null,
// Create program modal state
showCreateModal: false,
merchantSearch: '',
merchantResults: [],
selectedMerchant: null,
searchingMerchants: false,
// Search and filters
filters: {
search: '',
@@ -246,6 +257,69 @@ function adminLoyaltyPrograms() {
}
},
// Delete program
confirmDeleteProgram(program) {
this.deletingProgram = program;
this.showDeleteModal = true;
},
async deleteProgram() {
if (!this.deletingProgram) return;
try {
await apiClient.delete(`/admin/loyalty/programs/${this.deletingProgram.id}`);
Utils.showToast('Program deleted successfully', 'success');
loyaltyProgramsLog.info('Program deleted:', this.deletingProgram.id);
this.showDeleteModal = false;
this.deletingProgram = null;
await Promise.all([this.loadPrograms(), this.loadStats()]);
} catch (error) {
Utils.showToast(`Failed to delete program: ${error.message}`, 'error');
loyaltyProgramsLog.error('Failed to delete program:', error);
this.showDeleteModal = false;
this.deletingProgram = null;
}
},
// Search merchants for create modal
searchMerchants() {
if (this._merchantSearchTimeout) {
clearTimeout(this._merchantSearchTimeout);
}
this.selectedMerchant = null;
this._merchantSearchTimeout = setTimeout(async () => {
if (!this.merchantSearch || this.merchantSearch.length < 2) {
this.merchantResults = [];
return;
}
this.searchingMerchants = true;
try {
const params = new URLSearchParams();
params.append('search', this.merchantSearch);
params.append('limit', 10);
const response = await apiClient.get(`/admin/merchants?${params}`);
this.merchantResults = response.merchants || response || [];
loyaltyProgramsLog.info(`Found ${this.merchantResults.length} merchants`);
} catch (error) {
loyaltyProgramsLog.error('Merchant search failed:', error);
this.merchantResults = [];
} finally {
this.searchingMerchants = false;
}
}, 300);
},
// Check if a merchant already has a program in the loaded list
existingProgramForMerchant(merchantId) {
if (!merchantId) return false;
return this.programs.some(p => p.merchant_id === merchantId);
},
// Navigate to create program page for selected merchant
goToCreateProgram() {
if (!this.selectedMerchant) return;
window.location.href = `/admin/loyalty/merchants/${this.selectedMerchant.id}/program`;
},
// Format date for display
formatDate(dateString) {
if (!dateString) return 'N/A';