CRM Feature Plan · crm-v1.2

CRM Segments — v1.2

Smart customer groups inside the admin CRM. Define filter-based dynamic segments that resolve on demand — target by activity, spend, POS purchases, programs, skill level, membership, RFM, and more. Full-page builder UI for a complex enough task.

Planning2026-03-2914 acceptance criteria17 filter fields

Overview

Key Decisions

Problem

Staff can search and filter the customer list but cannot save those groups or act on them. Every query is one-time.

Solution

Named, persistent filter-based segments. Define conditions once — evaluate live on demand. Segments feed directly into Campaigns as targeting audiences.

  • →AND-only rule logic — all conditions must match. Simple, no UX confusion.
  • →No pg_cron for now — segments evaluate on-demand (preview + save). Nightly refresh deferred to v1.3.
  • →Full-page builder at /crm/segments/new — dialog is too cramped for this many filter fields.
  • →Lightweight v1.2: no member persistence table needed for dynamic segments — evaluate at campaign send time.
  • →RFM, churn risk, POS purchases, programs, level, and coaching packages all in scope for v1.2.

UI Architecture

Full-Page Builder (not a dialog)

Full-page layout at /crm/segments/new — NOT a dialog or slide-over

  • •17 filter fields across 10 groups — a dialog is too cramped
  • •Extra options per field (program type sub-filter, certified toggle on level) need space
  • •Live preview, field group tabs/sections, and help text all need breathing room
  • •Full page = same pattern as campaign compose — complex form gets its own route
┌─────────────────────────────────────────────────────────────────┐
│ ← Segments   New Segment                          [Save] [Cancel]│
├───────────────────────────┬─────────────────────────────────────┤
│ LEFT PANEL (400px)        │ RIGHT PANEL (flex-1)                │
│                           │                                     │
│ Name ________________     │  Filter Groups (tabs or accordion)  │
│ Description ___________   │  Activity | Spend | POS | Programs  │
│                           │  Membership | Coaching | Level | RFM│
│ ─────────────────────     │                                     │
│ Active Conditions (N)     │  [+ Add condition]                  │
│                           │                                     │
│ • Last visit > 30 days ×  │  Each condition row:                │
│ • RFM: At Risk ×          │  [Group/Field ▾] [Op ▾] [Value ___]│
│ • Joined program: League × │  (with sub-options when available) │
│                           │                                     │
│ ─────────────────────     │                                     │
│ 🟢 128 customers match    │                                     │
│ [Preview 5 sample names]  │                                     │
└───────────────────────────┴─────────────────────────────────────┘

Filter Fields

17 Fields Across 10 Groups

Activity

Last Visit (days ago)

Total Bookings

Activity Rank

Spend

Total Lifetime Spend

POS Purchases

Purchased Specific Item

Purchased from Category

Membership

Has Active Membership

Membership Status

Programs

Has Joined Program

Program Type (sub-filter)

Coaching

Has Coaching Package

Skill Level

Skill Level Range (+ certified option)

Community

Belongs to Community

Profile

Joined (days ago)

Has CRM Tag

RFM

RFM Segment

Churn Risk

Activity
Last Visitops: gt, lt, gte, lte· number

Days since most recent confirmed booking or match. Enter 30 = last 30 days.

Activity
Total Bookings (lifetime)ops: gt, lt, gte, lte, eq· number
Activity
Activity Rankops: in· multiselect
Spend
Total Lifetime Spendops: gt, lt, gte, lte· number

Displayed in facility currency. Compared in minor units (satang/cents).

POS Purchases
Purchased Specific Itemops: eq· select

Item options loaded from pos_catalog_items for this facility. Matches any confirmed order.

POS Purchases
Purchased from Categoryops: in· multiselect

Category options loaded from pos_categories for this facility.

Membership
Has Active Membershipops: eq· bool
Membership
Membership Statusops: in· multiselect
Programs
Has Joined Programops: eq· bool+ sub-options

Optionally filter by program type using the sub-option below.

↳ Filter by program type (league, academy, open, etc.)
Programs
Program Typeops: in· multiselect

Shown only when 'Has Joined Program' is active. Further filters by program type.

Coaching
Has Coaching Packageops: eq· bool

Active coaching package with remaining sessions.

Skill Level
Skill Level Rangeops: between· range+ sub-options

Enter min and max. E.g. 3.0–4.5 = intermediate players.

↳ Certified players only
Community
Belongs to Communityops: in· multiselect

Community options loaded from communities table for this facility.

Profile
Joined (days ago)ops: lt, gt· number

lt 30 = joined in last 30 days (new customers).

Profile
Has CRM Tagops: eq· select

Tag options loaded from crm_tags for this facility.

RFM
RFM Segmentops: in· multiselect
RFM
Churn Riskops: in· multiselect

Examples

What You Can Build

High-value inactive

Total spend > ฿20k AND last visit > 45 days

Use case: Win-back campaign

At-risk members

Has active membership AND churn risk = high/critical

Use case: Retention offer

Sports shop buyers

Purchased from category = Equipment AND last visit < 30 days

Use case: Upsell new gear

League players

Has joined program (type = league)

Use case: League announcement

Intermediate certified players

Level range 3.0–5.0 AND certified only

Use case: Advanced clinic invite

Coaching package holders

Has coaching package = yes

Use case: Coach schedule update

Community VIP group

Belongs to community = VIP Members

Use case: Exclusive event invite

New players, no membership

Joined < 30 days AND has membership = no

Use case: Membership conversion

Screens

3 Routes

/[facility]/crm/segmentsSegments List
  • • Segment cards: name, filter count, member count preview, edit/delete menu
  • • Empty state with CTA
  • • [+ New Segment] → /crm/segments/new
/[facility]/crm/segments/newNew Segment (full-page builder)
  • • Left panel: name, description, active conditions list, live preview count + 5 sample names
  • • Right panel: field group tabs, condition rows (field ▾ + operator ▾ + value input)
  • • Sub-options: program type picker appears when 'Has Joined Program' is added; certified toggle appears on Level Range
  • • [Save Segment] disabled until name + ≥1 condition
/[facility]/crm/segments/[id]/editEdit Segment (full-page builder)
  • • Same layout as New — prefilled with existing name, description, filters
  • • [Save Changes] + [Delete Segment] (danger, confirm dialog)

Database

crm_segments Table

CREATE TABLE crm_segments (
  id          UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  facility_id UUID        NOT NULL REFERENCES facilities(id) ON DELETE CASCADE,
  name        TEXT        NOT NULL CHECK (char_length(name) BETWEEN 1 AND 80),
  description TEXT,
  filters     JSONB       NOT NULL DEFAULT '[]',
  created_by  UUID        REFERENCES customers(id),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (facility_id, name)
);

Filters JSONB example

[
  {
    "field": "last_visit_days",
    "operator": "gt",
    "value": 30
  },
  {
    "field": "rfm_segment",
    "operator": "in",
    "value": [
      "at_risk"
    ]
  },
  {
    "field": "has_joined_program",
    "operator": "eq",
    "value": true,
    "meta": {
      "program_type": [
        "league"
      ]
    }
  },
  {
    "field": "level_range",
    "operator": "between",
    "value": [
      3,
      5
    ],
    "meta": {
      "certified_only": true
    }
  }
]

API

6 Routes

GET/api/admin/crm/segmentsList all segments
POST/api/admin/crm/segmentsCreate segment
GET/api/admin/crm/segments/[id]Get segment detail
PUT/api/admin/crm/segments/[id]Update segment
DELETE/api/admin/crm/segments/[id]Delete segment
POST/api/admin/crm/segments/previewLive preview — count matching customers without saving

Acceptance Criteria

14 Items

Segments list page renders cards with name, filter count, and action menu
Empty state renders with CTA when no segments exist
New segment opens as full-page route (not dialog)
Filter field dropdown is grouped across 10 groups
'Has Joined Program' shows program-type sub-filter when active
'Skill Level Range' shows certified-only toggle
Live preview shows matching count + 5 sample names (debounced 600ms)
POS item filter options load from pos_catalog_items for this facility
POS category filter options load from pos_categories for this facility
Community filter loads options from communities table
Saving stores filters as JSONB, segment appears in list
Edit page prefills correctly; saving updates the segment
Delete with confirmation removes segment and redirects to list
Segment preview in campaign targeting shows correct recipient count