feat(loyalty): add paginated transaction history to card detail
Some checks failed
CI / ruff (push) Successful in 15s
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 been cancelled

The store card detail page now shows paginated transaction history
instead of a flat list of 50. Uses PlatformSettings.getRowsPerPage()
for the page size (default 20), with Previous/Next navigation and
"Page X of Y" indicator using server-rendered i18n.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 22:13:00 +02:00
parent 64fe58c171
commit 914967edcc
3 changed files with 44 additions and 4 deletions

View File

@@ -761,7 +761,8 @@
"col_location": "Location", "col_location": "Location",
"col_notes": "Notes", "col_notes": "Notes",
"no_transactions": "No transactions yet", "no_transactions": "No transactions yet",
"card_label": "Card" "card_label": "Card",
"page_x_of_y": "Page {page} of {pages}"
}, },
"enroll": { "enroll": {
"title": "Enroll Customer", "title": "Enroll Customer",

View File

@@ -11,6 +11,7 @@ function storeLoyaltyCardDetail() {
cardId: null, cardId: null,
card: null, card: null,
transactions: [], transactions: [],
txPagination: { page: 1, perPage: 20, total: 0, pages: 0 },
loading: false, loading: false,
error: null, error: null,
@@ -38,6 +39,13 @@ function storeLoyaltyCardDetail() {
return; return;
} }
// Use platform pagination setting if available
if (window.PlatformSettings) {
try {
this.txPagination.perPage = await window.PlatformSettings.getRowsPerPage();
} catch (e) { /* use default */ }
}
await this.loadData(); await this.loadData();
loyaltyCardDetailLog.info('=== LOYALTY CARD DETAIL PAGE INITIALIZATION COMPLETE ==='); loyaltyCardDetailLog.info('=== LOYALTY CARD DETAIL PAGE INITIALIZATION COMPLETE ===');
}, },
@@ -67,18 +75,32 @@ function storeLoyaltyCardDetail() {
} }
}, },
async loadTransactions() { async loadTransactions(page = 1) {
try { try {
const response = await apiClient.get(`/store/loyalty/cards/${this.cardId}/transactions?limit=50`); const skip = (page - 1) * this.txPagination.perPage;
const response = await apiClient.get(
`/store/loyalty/cards/${this.cardId}/transactions?skip=${skip}&limit=${this.txPagination.perPage}`
);
if (response && response.transactions) { if (response && response.transactions) {
this.transactions = response.transactions; this.transactions = response.transactions;
loyaltyCardDetailLog.info(`Loaded ${this.transactions.length} transactions`); this.txPagination.total = response.total || 0;
this.txPagination.page = page;
this.txPagination.pages = Math.ceil(this.txPagination.total / this.txPagination.perPage);
loyaltyCardDetailLog.info(`Loaded ${this.transactions.length} of ${this.txPagination.total} transactions (page ${page})`);
} }
} catch (error) { } catch (error) {
loyaltyCardDetailLog.warn('Failed to load transactions:', error.message); loyaltyCardDetailLog.warn('Failed to load transactions:', error.message);
} }
}, },
txPreviousPage() {
if (this.txPagination.page > 1) this.loadTransactions(this.txPagination.page - 1);
},
txNextPage() {
if (this.txPagination.page < this.txPagination.pages) this.loadTransactions(this.txPagination.page + 1);
},
formatNumber(num) { formatNumber(num) {
return num == null ? '0' : new Intl.NumberFormat('en-US').format(num); return num == null ? '0' : new Intl.NumberFormat('en-US').format(num);
}, },

View File

@@ -151,6 +151,23 @@
</template> </template>
</tbody> </tbody>
{% endcall %} {% endcall %}
<!-- Pagination -->
<div x-show="txPagination.pages > 1" class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<button @click="txPreviousPage()" :disabled="txPagination.page <= 1"
type="button"
class="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50">
{{ _('loyalty.common.previous') }}
</button>
<span class="text-sm text-gray-500 dark:text-gray-400"
x-text="'{{ _('loyalty.store.card_detail.page_x_of_y') }}'.replace('{page}', txPagination.page).replace('{pages}', txPagination.pages)">
</span>
<button @click="txNextPage()" :disabled="txPagination.page >= txPagination.pages"
type="button"
class="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50">
{{ _('loyalty.common.next') }}
</button>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}