Skip to main content

Learning Goals System

While parents create and manage rewards, children interact with them as learning goals they can work toward. This page explains the learner-facing goal system from a parent’s perspective.

How Goals Work

From the learner’s perspective:
  1. View available rewards in the Goals page
  2. Save points toward rewards they want
  3. Track progress with visual progress bars
  4. Cash out when they have enough points saved
  5. Wait for approval from the parent
From learner-goals-page.tsx:201-265:
const LearnerGoalsPage: React.FC = () => {
  const { selectedLearner } = useMode();
  const learnerId = selectedLearner?.id ?? 0;
  const [tab, setTab] = useState<'goals' | 'history'>('goals');
  
  const { data: goals = [] } = useQuery<RewardGoal[]>({
    queryKey: ['/api/rewards', learnerId],
    queryFn: () => apiRequest('GET', `/api/rewards?learnerId=${learnerId}`).then(r => r.data),
    enabled: !!learnerId,
  });
  
  const { data: balanceData } = useQuery({
    queryKey: ['/api/points/balance', learnerId],
    queryFn: () => apiRequest('GET', `/api/points/balance?learnerId=${learnerId}`).then(r => r.data),
    enabled: !!learnerId,
  });
};

The Learner Interface

Header with Balance

From learner-goals-page.tsx:242-252:
<View style={styles.header}>
  <TouchableOpacity onPress={() => setLocation('/learner')}>
    <ArrowLeft size={22} color={theme.colors.textPrimary} />
  </TouchableOpacity>
  <Gift size={20} color={theme.colors.primary} />
  <Text style={styles.headerTitle}>My Goals</Text>
  <View style={{ flex: 1 }} />
  <View style={styles.balanceBadge}>
    <Text style={styles.balanceText}>⭐ {balance} pts</Text>
  </View>
</View>
The balance shown includes only available points, not points already saved toward other goals.

Goal Cards

Each goal displays comprehensive information: From learner-goals-page.tsx:40-114:
const GoalCard: React.FC<{ goal: RewardGoal }> = ({ goal, learnerId, onSavePoints, onRedeem, isProcessing }) => {
  const saved = goal.savedPoints ?? 0;
  const pct = goal.tokenCost > 0 ? Math.min(100, Math.round((saved / goal.tokenCost) * 100)) : 0;
  const isComplete = saved >= goal.tokenCost;

  return (
    <View style={[styles.card, { borderLeftColor: goal.color, borderLeftWidth: 5 }]}>
      {/* Header with emoji and title */}
      <View style={styles.cardHeader}>
        <View style={[styles.emojiCircle, { backgroundColor: goal.color + '22' }]}>
          <Text style={{ fontSize: 32 }}>{goal.imageEmoji}</Text>
        </View>
        <View style={{ flex: 1, marginLeft: 12 }}>
          <Text style={styles.cardTitle}>{goal.title}</Text>
          {goal.description && (
            <Text style={styles.cardDesc}>{goal.description}</Text>
          )}
        </View>
        {isComplete && !goal.hasPendingRedemption && (
          <View style={[styles.readyBadge, { backgroundColor: goal.color }]}>
            <Text style={styles.readyBadgeText}>Ready!</Text>
          </View>
        )}
      </View>

      {/* Progress bar */}
      <View style={styles.progressSection}>
        <View style={styles.progressLabelRow}>
          <Text style={styles.progressLabel}>
            {saved} / {goal.tokenCost} pts saved
          </Text>
          <Text style={[styles.progressPct, { color: goal.color }]}>{pct}%</Text>
        </View>
        <View style={[styles.progressTrack, { backgroundColor: theme.colors.divider }]}>
          <View style={[styles.progressFill, { width: `${pct}%`, backgroundColor: goal.color }]} />
        </View>
      </View>

      {/* Action buttons */}
      <View style={styles.cardActions}>
        {!isComplete && !goal.hasPendingRedemption && (
          <TouchableOpacity
            style={[styles.actionBtn, { backgroundColor: goal.color + '20', borderColor: goal.color }]}
            onPress={() => onSavePoints(goal.id)}
          >
            <Text style={[styles.actionBtnText, { color: goal.color }]}>💰 Save Points</Text>
          </TouchableOpacity>
        )}
        {isComplete && !goal.hasPendingRedemption && (
          <TouchableOpacity
            style={[styles.actionBtn, { backgroundColor: goal.color }]}
            onPress={() => onRedeem(goal.id)}
          >
            <Text style={[styles.actionBtnText, { color: '#fff' }]}>🎉 Cash Out!</Text>
          </TouchableOpacity>
        )}
      </View>
    </View>
  );
};

Saving Points

The Save Points Modal

When a child clicks “Save Points”, they see a modal to choose how many points to commit: From learner-goals-page.tsx:116-197:
const SavePointsModal: React.FC<{ goal: RewardGoal | null; balance: number }> = ({
  goal, balance, learnerId, visible, onClose
}) => {
  const [points, setPoints] = useState(0);

  const saveMutation = useMutation({
    mutationFn: (pts: number) =>
      apiRequest('POST', `/api/rewards/${goal!.id}/save?learnerId=${learnerId}`, { points: pts })
        .then(r => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['/api/rewards'] });
      queryClient.invalidateQueries({ queryKey: ['/api/points/balance'] });
      onClose();
    },
  });

  if (!goal) return null;

  const saved = goal.savedPoints ?? 0;
  const remaining = Math.max(0, goal.tokenCost - saved);
  const maxSave = Math.min(balance, remaining);
  const presets = [1, 5, 10, maxSave].filter((v, i, a) => v > 0 && a.indexOf(v) === i);

  return (
    <Modal visible={visible} transparent onRequestClose={onClose}>
      <View style={styles.modalBox}>
        <Text style={styles.modalTitle}>
          Save to {goal.imageEmoji} {goal.title}
        </Text>
        <Text style={styles.modalSub}>
          Balance: {balance} pts · {remaining > 0 ? `Need ${remaining} more` : '✓ Goal reached!'}
        </Text>

        {/* Preset buttons */}
        <View style={styles.presetRow}>
          {presets.map(p => (
            <TouchableOpacity key={p}
              style={[styles.presetBtn, points === p && { backgroundColor: goal.color }]}
              onPress={() => setPoints(p)}
            >
              <Text style={[styles.presetBtnText, points === p && { color: '#fff' }]}>{p}</Text>
            </TouchableOpacity>
          ))}
        </View>

        {/* Custom stepper */}
        <View style={styles.customRow}>
          <TouchableOpacity onPress={() => setPoints(Math.max(0, points - 1))}>
            <Text>−</Text>
          </TouchableOpacity>
          <Text style={[styles.pointsDisplay, { color: goal.color }]}>{points}</Text>
          <TouchableOpacity onPress={() => setPoints(Math.min(maxSave, points + 1))}>
            <Text>+</Text>
          </TouchableOpacity>
        </View>

        <TouchableOpacity
          style={[styles.modalBtn, { backgroundColor: goal.color }]}
          onPress={() => saveMutation.mutate(points)}
          disabled={points <= 0}
        >
          <Text>Save {points} pts</Text>
        </TouchableOpacity>
      </View>
    </Modal>
  );
};

How Point Saving Works

1

Child Selects Amount

Uses preset buttons (1, 5, 10, or max) or custom stepperThe maximum they can save is:
const maxSave = Math.min(balance, remaining);
Whichever is less: available balance or points still needed
2

Points Transfer

When they click “Save”:
  1. Points deducted from available balance
  2. Points added to saved total for that reward
  3. Progress bar updates immediately
  4. Balance badge updates
3

Working Toward Goal

Child continues earning and saving until:
const isComplete = saved >= goal.tokenCost;
When complete, the “Cash Out” button appears
Points saved toward a goal are locked to that goal. Children can’t use them elsewhere unless the parent deletes the reward (which refunds the points) or they redeem it.

Cashing Out (Redemption)

When a child has enough points saved: From learner-goals-page.tsx:227-234:
const redeemMutation = useMutation({
  mutationFn: (rewardId: string) =>
    apiRequest('POST', `/api/rewards/${rewardId}/redeem?learnerId=${learnerId}`).then(r => r.data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['/api/rewards', learnerId] });
    queryClient.invalidateQueries({ queryKey: ['/api/redemptions/my', learnerId] });
  },
});
1

Click Cash Out

The child clicks the ”🎉 Cash Out!” button on the goal card
2

Create Redemption Request

System creates a redemption with status PENDING
3

Wait for Parent

The goal card shows:
{goal.hasPendingRedemption && (
  <View style={styles.readyBadge}>
    <Text>Waiting</Text>
  </View>
)}
And a message:
<Text>⏳ Waiting for parent approval</Text>
4

Parent Approves/Rejects

From the parent Rewards page, you can:
  • Approve - Points are spent, child receives reward
  • Reject - Points refunded to available balance

Redemption History

Children can view their past redemptions in the History tab: From learner-goals-page.tsx:292-328:
{tab === 'history' && (
  <>
    {redemptions.length === 0 ? (
      <View style={styles.empty}>
        <Text style={{ fontSize: 52 }}>📭</Text>
        <Text style={styles.emptyTitle}>No history yet</Text>
        <Text style={styles.emptyDesc}>
          Cash out your first reward to see it here!
        </Text>
      </View>
    ) : redemptions.map(r => (
      <View key={r.id} style={[styles.historyCard, { borderLeftColor: r.rewardColor }]}>
        <View style={styles.cardHeader}>
          <Text style={{ fontSize: 28 }}>{r.rewardEmoji}</Text>
          <View style={{ flex: 1 }}>
            <Text style={styles.cardTitle}>{r.rewardTitle}</Text>
            <Text style={styles.cardDesc}>
              {r.tokensSpent} pts · {new Date(r.requestedAt).toLocaleDateString()}
            </Text>
          </View>
          <View style={[styles.statusBadge, {
            backgroundColor: r.status === 'APPROVED' ? '#E8F5E9' :
                              r.status === 'REJECTED' ? '#FFEBEE' : '#FFF8E1'
          }]}>
            <Text>
              {r.status === 'APPROVED' ? '✓ Approved' :
               r.status === 'REJECTED' ? '✗ Rejected' : '⏳ Pending'}
            </Text>
          </View>
        </View>
        {r.parentNotes && (
          <Text style={styles.cardDesc}>
            Parent: {r.parentNotes}
          </Text>
        )}
      </View>
    ))}
  </>
)}

Parent Notes

When approving or rejecting redemptions, parents can include notes that children see in their history:
{r.parentNotes && (
  <Text>Parent: {r.parentNotes}</Text>
)}
Example notes:
  • “Great job! We’ll go this weekend.”
  • “Let’s wait until after your homework is done.”
  • “Pick which game you want and we’ll get it.”

Understanding the Two-Balance System

Sunschool uses two separate point balances:

Available Balance

Points the child can spend freely:
  • Save toward any goal
  • Use for other features
  • Shown in the header badge

Saved (Locked) Points

Points committed to specific goals:
  • Locked to that reward
  • Count toward goal progress
  • Can’t be used elsewhere
From the API:
// Total points earned
const totalPoints = /* sum of all point transactions */;

// Points saved toward goals
const savedPoints = /* sum of points saved to rewards */;

// Available balance
const balance = totalPoints - savedPoints;

Goal States

Goals progress through several states:
1

In Progress

!isComplete && !hasPendingRedemption
Shows ”💰 Save Points” button
2

Complete

isComplete && !hasPendingRedemption
Shows ”🎉 Cash Out!” button with “Ready!” badge
3

Pending Approval

hasPendingRedemption
Shows “Waiting…” badge and waiting message
4

Approved

Appears in history as approved, points spent
5

Rejected

Points refunded, goal returns to previous state

Empty States

If no goals are available: From learner-goals-page.tsx:272-278:
{goals.length === 0 ? (
  <View style={styles.empty}>
    <Text style={{ fontSize: 52 }}>🎁</Text>
    <Text style={styles.emptyTitle}>No goals yet!</Text>
    <Text style={styles.emptyDesc}>
      Ask your parent to add some rewards for you to earn.
    </Text>
  </View>
) : (
  // Show goal cards
)}

Best Practices for Parents

Ensure point costs are reasonable for your child’s engagement level. A child who earns 5-10 points per day shouldn’t face 500-point goals.
When children request redemptions, respond within a day or two. This keeps them motivated and trusting the system.
Even when rejecting (rare cases), include a kind explanation:
  • “Let’s save this for after your test”
  • “Great saving! Let’s do this on the weekend”
Use the learner progress feature on reward cards to see what motivates each child. Some might focus on one big goal, others spread points across many.
When a child reaches 50% or 75% toward a goal, acknowledge their progress. This builds momentum.

Troubleshooting

Check if they:
  1. Saved points toward a goal (shows in progress bar)
  2. Had a redemption approved (spent the points)
  3. Tried to save more than available (would fail silently)
Verify:
  1. Points are in “saved” balance, not available
  2. The goal isn’t inactive
  3. There isn’t already a pending redemption
  4. Page has been refreshed
Ensure:
  1. Points were actually saved (check available balance decreased)
  2. The save mutation succeeded (no errors)
  3. Page was refreshed after saving

Next Steps