Skip to main content

Functional Specification: Voucher Tabs Naming, Filtering & Expiration Sorting

Last Updated: 16/07/2026
Type: Functional Specification Upgrade — Voucher Management
Screen: My Vouchers (VoucherScreen) — Tab Navigation ("Chưa sử dụng", "Quà tặng", "Đã sử dụng", "Hết hạn")
Severity: 🔴 High — Reverts tab title to "Chưa sử dụng", specifies strict category sorting, and defines precise data movements across tab states

Feedback Screenshot:


1. Overview & Tab Naming Rollback

Following user review on the tab naming conventions:

  • The first tab should NOT be named "Tất cả" (All).
  • It must keep its original name: "Chưa sử dụng" (Unused), but act as a catch-all container for all active, unredeemed vouchers.

2. Dynamic Tab Rules & Expiration Sorting

┌──────────────────────────────┐
│ All Vouchers Database │
└──────────────┬───────────────┘

┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Status: Active │ │ Status: Used │ │ Status: Expired │
│ expiredAt > NOW │ │ │ │ expiredAt <= NOW│
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
┌─────┴──────────┐ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Chưa sử dụng │ │ Quà tặng │ │ Đã sử dụng │ │ Hết hạn │
│ (All Types) │ │ (Gifts Only) │ │ (All Types) │ │ (All Types) │
│ *Sort: │ │ *Filter: │ │ *Filter: │ │ *Filter: │
│ Shortest │ │ Gifts only │ │ used status │ │ expired │
│ expiry first│ │ │ │ │ │ status │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘

2.1 Tab 1: Chưa sử dụng (Unused — Default Tab)

  • Content: Displays all active, valid, and unredeemed vouchers owned by the user. This includes service vouchers, purchased vouchers, GPoint conversion vouchers, and gift/campaign vouchers.
  • Sorting Logic: Primary sort: Expiration date ascending (expiredAt ASC).
    • Vouchers with the shortest remaining validity period (closest to expiration) must sit at the very top of the list to prompt the user to use them before they expire.

2.2 Tab 2: Quà tặng (Gifts)

  • Content: Filters and displays only active, unredeemed vouchers classified as gifts (e.g. Birthday vouchers sent by Marketing, or prize vouchers won from the Lucky Spin).
  • Life Cycle/Flow:
    • While active and unused, the gift voucher shows in both "Chưa sử dụng" and "Quà tặng" tabs.
    • As soon as the gift voucher is redeemed (used) or expires (expired), it must be removed from the "Quà tặng" tab. It moves to the "Đã sử dụng" or "Hết hạn" tab respectively.

2.3 Tab 3: Đã sử dụng (Used)

  • Content: Displays the history of all vouchers (regular vouchers, conversions, and gifts) that have been successfully redeemed (status === 'used').

2.4 Tab 4: Hết hạn (Expired)

  • Content: Displays all vouchers that exceeded their expiration date without being used (status === 'active' AND expiredAt <= NOW()).

3. Data Processing Logic (React Native / Typescript)

Implement the filters and sorting keys inside VoucherScreen.tsx:

interface Voucher {
id: string;
code: string;
templateId: string;
status: 'active' | 'used';
expiredAt: string; // ISO string timestamp
validFrom: string; // ISO string timestamp
sourceType: 'marketing_gift' | 'lucky_spin' | 'point_redemption' | 'purchase' | 'birthday';
}

export const getTabFilteredVouchers = (vouchers: Voucher[]) => {
const now = new Date();

// Helper: Sort by expiration date ascending (closest expiration first)
const sortByExpiryAsc = (a: Voucher, b: Voucher) =>
new Date(a.expiredAt).getTime() - new Date(b.expiredAt).getTime();

// Helper: Sort by usage date descending (most recently used first, assuming usedAt exists)
const sortByUsageDesc = (a: Voucher, b: Voucher) =>
new Date(b.expiredAt).getTime() - new Date(a.expiredAt).getTime(); // Fallback to expiry

// 1. CHƯA SỬ DỤNG: Active unused, sorted by closest expiry
const unusedList = vouchers
.filter((v) => v.status === 'active' && new Date(v.expiredAt) > now)
.sort(sortByExpiryAsc);

// 2. QUÀ TẶNG: Active unused, filtered to gift source types
const giftSources = ['lucky_spin', 'marketing_gift', 'birthday'];
const giftList = vouchers
.filter((v) => v.status === 'active' &&
new Date(v.expiredAt) > now &&
giftSources.includes(v.sourceType))
.sort(sortByExpiryAsc);

// 3. ĐÃ SỬ DỤNG: History of used vouchers
const usedList = vouchers
.filter((v) => v.status === 'used')
.sort(sortByUsageDesc);

// 4. HẾT HẠN: Non-redeemed but expired
const expiredList = vouchers
.filter((v) => v.status === 'active' && new Date(v.expiredAt) <= now)
.sort(sortByUsageDesc);

return {
unused: unusedList,
gifts: giftList,
used: usedList,
expired: expiredList
};
};

Technical Checklist

  • Rename the first tab back to "Chưa sử dụng" (instead of "Tất cả").
  • Implement the sortByExpiryAsc sorting rule to display closest expiring vouchers at the top of both "Chưa sử dụng" and "Quà tặng" lists.
  • Ensure gift vouchers automatically disappear from the "Quà tặng" tab when they transit to used or expired states.
  • Verify that all vouchers (regardless of source) are shown in the "Đã sử dụng" and "Hết hạn" lists when they transition.
  • Test cases:
    • Add Voucher A (expires in 2 days) and Voucher B (expires in 10 days). Open the "Chưa sử dụng" tab → verify Voucher A is rendered above Voucher B.
    • Redeem Voucher A → verify it vanishes from "Chưa sử dụng" and appears in the "Đã sử dụng" tab.
    • Let a Lucky Spin Voucher expire → verify it vanishes from both "Chưa sử dụng" and "Quà tặng" tabs, and appears in the "Hết hạn" tab.