Phase 3: P2/P3 Polish — Membership Form & Role Consistency
> Execution Document for Claude CLI Agent
> Goal: Fix 4 lower-priority bugs: 1 P2 (missing form field) and 3 P3 (cosmetic/labeling).
> Scope: Frontend components, utils, backend schema/handlers
> Dependencies: BUG-PHASE7-004 depends on BUG-PHASE7-002 (Phase 1) being fixed first. All others are independent.
Bug 1: BUG-PHASE7-003 — Membership Form Shows "undefined" Instead of Org Name
Root Cause
MembershipApplicationPage.tsx references branding.orgName at lines 370, 375, and 376. The BrandingConfig interface in mmp/frontend/pwa-app/src/config/branding.ts does NOT have an orgName property — only appName. Accessing branding.orgName returns JavaScript undefined, which is rendered as the literal string "undefined" in template literals.
The useTenantConfig() hook (in mmp/frontend/pwa-app/src/hooks/useTenantConfig.ts) fetches the actual organization name from the backend and provides organizationName. However, this hook requires authentication — so for unauthenticated invitation-link users, a fallback is needed.
Files to Modify
mmp/frontend/pwa-app/src/pages/MembershipApplicationPage.tsx(lines ~370, 375, 376)
Exact Changes
Step 1: Add the import for useTenantConfig at the top of MembershipApplicationPage.tsx:
import { useTenantConfig } from "../hooks/useTenantConfig";
Step 2: Inside the component function (after the existing useBranding() call at line 35), add:
const { config: tenantConfig } = useTenantConfig();
const orgName = tenantConfig?.organizationName && tenantConfig.organizationName !== "Organization"
? tenantConfig.organizationName
: branding.appName;
Step 3: Replace all occurrences of branding.orgName with orgName:
Find (line ~370): alt={branding.orgName} → Replace with: alt={orgName}
Find (line ~375): ` You've been invited to join ${branding.orgName} → Replace with: You've been invited to join ${orgName} `
Find (line ~376): ` Complete your application to join ${branding.orgName} → Replace with: Complete your application to join ${orgName} `
Note: Search for ALL occurrences of branding.orgName in the file and replace them. There may be additional occurrences beyond lines 370/375/376.
Verification
- Frontend hot-reloads
- Log in as guest (
guest@demo.membervu.com/Guest123!) - Click "Apply for Membership"
- Expected: Form header shows "Complete your application to join Rotary Club of Manila Expats" (or the actual org name)
- Not expected: "Complete your application to join undefined"
Bug 2: BUG-PHASE3-002 — Missing Role Display Labels
Root Cause
ROLE_DISPLAY_LABELS in mmp/frontend/pwa-app/src/utils/roles.ts (lines 14-26) is missing an entry for PENDING_PAYMENT. The getPrimaryRole() function (lines 191-202) also omits PENDING_PAYMENT and EXPIRED_MEMBER from its priority order.
Current ROLE_DISPLAY_LABELS has:
PENDING_MEMBER: "Pending Member"
EXPIRED_MEMBER: "Expired Member" ← present
GUEST: "Guest"
Missing: PENDING_PAYMENT
Current getPrimaryRole priority order ends with:
MEMBER, PENDING_MEMBER, GUEST
Missing: PENDING_PAYMENT and EXPIRED_MEMBER
Files to Modify
mmp/frontend/pwa-app/src/utils/roles.ts(lines 14-26 and 191-202)mmp/frontend/pwa-app/src/utils/viewLabel.ts(add PENDING_PAYMENT handling)
Exact Changes
roles.ts — ROLE_DISPLAY_LABELS (line 25, between EXPIRED_MEMBER and GUEST):
Find:
[UserRole.EXPIRED_MEMBER]: "Expired Member",
[UserRole.GUEST]: "Guest",
Replace with:
[UserRole.EXPIRED_MEMBER]: "Expired Member",
[UserRole.PENDING_PAYMENT]: "Pending Payment",
[UserRole.GUEST]: "Guest",
Note: Verify that UserRole.PENDING_PAYMENT exists in the UserRole enum at mmp/frontend/pwa-app/src/types/roles.ts. If it doesn't, add it there first.
roles.ts — getPrimaryRole priority (lines 191-202):
Find:
const priorityOrder = [
UserRole.SUPER_ADMIN,
UserRole.ADMIN,
UserRole.OFFICER,
UserRole.FINANCE_MANAGER,
UserRole.EVENT_MANAGER,
UserRole.COMMUNICATIONS_MANAGER,
UserRole.MEMBERSHIP_MANAGER,
UserRole.MEMBER,
UserRole.PENDING_MEMBER,
UserRole.GUEST,
];
Replace with:
const priorityOrder = [
UserRole.SUPER_ADMIN,
UserRole.ADMIN,
UserRole.OFFICER,
UserRole.FINANCE_MANAGER,
UserRole.EVENT_MANAGER,
UserRole.COMMUNICATIONS_MANAGER,
UserRole.MEMBERSHIP_MANAGER,
UserRole.MEMBER,
UserRole.PENDING_MEMBER,
UserRole.PENDING_PAYMENT,
UserRole.EXPIRED_MEMBER,
UserRole.GUEST,
];
viewLabel.ts — Find the file at mmp/frontend/pwa-app/src/utils/viewLabel.ts and look for where EXPIRED_MEMBER view label is handled. Add a similar block for PENDING_PAYMENT:
if (normalizedRoles.includes("PENDING_PAYMENT")) {
return "Pending Payment View";
}
Place it near the existing EXPIRED_MEMBER check (they should be adjacent for readability).
Verification
- Frontend hot-reloads
- Check that the TypeScript compiler has no errors
- If possible, log in as a user who has PENDING_PAYMENT status and verify the sidebar shows "Pending Payment View"
- The ROLE_DISPLAY_LABELS change will be visible in any UI that displays role labels (e.g., Admin > Members > member detail page, role badges)
Bug 3: BUG-PHASE7-004 — Membership Form Missing "Membership Type" Field
Root Cause
The membership application form in MembershipApplicationPage.tsx collects personal info, professional info, bio, and motivation — but NOT membership type. The backend submitApplication service doesn't accept a membership type preference either. The 3 membership types exist in seed data:
- Regular Member: PHP 15,000
- Associate Member: PHP 10,000
- Student Member: PHP 5,000
Currently, the admin manually selects membership type during the approval step. The fix adds a "preferred membership type" field so applicants can indicate their preference.
Important: This bug depends on BUG-PHASE7-002 (Phase 1) being fixed first — the application form must be submittable by guests.
Files to Modify
mmp/auth-service/prisma/schema.prisma— Add field to Member modelmmp/auth-service/src/services/membership/membershipApplication.service.ts— Accept preferred typemmp/auth-service/src/handlers/membershipApplicationHandlers.ts— Extract from request bodymmp/auth-service/src/server.ts— Add public membership types endpointmmp/frontend/pwa-app/src/pages/MembershipApplicationPage.tsx— Add dropdown to formmmp/frontend/pwa-app/src/api/client.ts— Add type to submission payload
Exact Changes
Step 1: Backend Schema (prisma/schema.prisma)
Find the Member model and add a new optional field:
preferredMembershipTypeId String?
Add it near other membership-related fields (e.g., near membershipTypeId). Do NOT add a relation — this is just a preference string, not a foreign key constraint.
Run migration:
docker exec membervu-backend npx prisma migrate dev --name add-preferred-membership-type
Or if running locally:
cd mmp/auth-service && npx prisma migrate dev --name add-preferred-membership-type
Step 2: Backend Service (membershipApplication.service.ts)
Find the ApplicationSubmission interface/type (around line 23-34). Add:
preferredMembershipTypeId?: string;
Find the submitApplication function where it creates/updates the member record. Add preferredMembershipTypeId to the data:
preferredMembershipTypeId: data.preferredMembershipTypeId || null,
Step 3: Backend Handler (membershipApplicationHandlers.ts)
At line 44, add preferredMembershipTypeId to the destructured request body:
const { invitationToken, bio, motivation, firstName, lastName, phone, address, profession, company, linkedinUrl, preferredMembershipTypeId } = req.body;
Pass it through to submitApplication at line 107-122:
const result = await submitApplication(
targetTenantId,
targetMemberId,
{
bio: bio.trim(),
motivation: motivation.trim(),
firstName,
lastName,
phone,
address,
profession,
company,
linkedinUrl,
preferredMembershipTypeId,
},
invitationToken
);
Step 4: Public Membership Types Endpoint (server.ts)
Add a new public route BEFORE the auth middleware section (near line 448, with the other public routes):
// Public membership types (for application form)
app.get("/api/public/:tenantSlug/membership-types", async (req, res) => {
try {
const tenant = await prisma.tenant.findFirst({
where: { slug: req.params.tenantSlug },
});
if (!tenant) return res.status(404).json({ error: "Tenant not found" });
const types = await prisma.membershipType.findMany({
where: { tenantId: tenant.id, isActive: true },
select: { id: true, name: true, description: true, amountCents: true, currency: true, period: true },
orderBy: { amountCents: "desc" },
});
return res.json({ membershipTypes: types });
} catch (err) {
return res.status(500).json({ error: "Failed to fetch membership types" });
}
});
Add the prisma import if not already present (it should be — check line 12: import { prisma } from "./db/prisma";).
Step 5: Frontend Form (MembershipApplicationPage.tsx)
Add to form state (line 43-54):
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
address: "",
profession: "",
company: "",
linkedinUrl: "",
bio: "",
motivation: "",
preferredMembershipTypeId: "", // NEW
});
Add state for membership types:
const [membershipTypes, setMembershipTypes] = useState<Array<{
id: string;
name: string;
description: string | null;
amountCents: number;
currency: string;
period: string;
}>>([]);
Add useEffect to fetch membership types on mount:
useEffect(() => {
const tenantSlug = import.meta.env.VITE_DEFAULT_TENANT_ID || "rcme";
fetch(${import.meta.env.VITE_API_BASE_URL || ""}/api/public/${tenantSlug}/membership-types)
.then(res => res.json())
.then(data => {
if (data.membershipTypes) setMembershipTypes(data.membershipTypes);
})
.catch(err => console.error("Failed to load membership types:", err));
}, []);
Add the dropdown in the form JSX, between the "Professional Information" section and the "Application Statement" section. Look for a natural break point (typically a section divider or heading):
{/* Membership Type Selection */}
{membershipTypes.length > 0 && (
<div className="space-y-1">
<label className="block text-sm font-medium text-text-primary">
Preferred Membership Type <span className="text-red-500">*</span>
</label>
<select
name="preferredMembershipTypeId"
value={form.preferredMembershipTypeId}
onChange={(e) => {
setForm({ ...form, preferredMembershipTypeId: e.target.value });
if (errors.preferredMembershipTypeId) {
setErrors({ ...errors, preferredMembershipTypeId: "" });
}
}}
className="w-full px-3 py-2 border border-border rounded-lg bg-surface-1 text-text-primary focus:ring-2 focus:ring-primary/50 focus:border-primary"
>
<option value="">Select a membership type...</option>
{membershipTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.name} — {type.currency} {(type.amountCents / 100).toLocaleString()} / {type.period}
</option>
))}
</select>
{errors.preferredMembershipTypeId && (
<p className="text-sm text-red-500">{errors.preferredMembershipTypeId}</p>
)}
</div>
)}
Add validation in the form's validate/submit handler:
if (membershipTypes.length > 0 && !form.preferredMembershipTypeId) {
errs.preferredMembershipTypeId = "Please select a membership type";
}
Include preferredMembershipTypeId in the submission payload (find where submitMembershipApplication is called and add it to the data object).
Step 6: Frontend API (client.ts)
Find the submitMembershipApplication function or the type for its payload. Add preferredMembershipTypeId?: string to the submission interface/payload.
Verification
- Run Prisma migration (see Step 1 above)
- Restart backend:
docker restart membervu-backend - Frontend hot-reloads
- Log in as guest (
guest@demo.membervu.com/Guest123!) - Click "Apply for Membership"
- Expected: Form now shows "Preferred Membership Type" dropdown with 3 options:
- Regular Member — PHP 15,000 / ANNUAL
- Associate Member — PHP 10,000 / ANNUAL
- Student Member — PHP 5,000 / ANNUAL
- Select a type, fill out remaining fields, submit
- Expected: Application submitted successfully
- Log in as admin, check the application — preferred membership type should be stored
Bug 4: BUG-PHASE3-001 — Quick Login Buttons Don't Reflect Updated Roles
Root Cause
The DEMO_ACCOUNTS object in LoginPage.tsx (lines 58-179) has hardcoded role and displayName properties. These are static demo/development labels — after login, the actual server-side roles are fetched via refreshSession. The button labels never update because they're not connected to the database.
This is largely by design for demo convenience buttons. The fix is to ensure labels match the current seed data and add a clarifying comment.
Files to Modify
mmp/frontend/pwa-app/src/pages/LoginPage.tsx(lines 58-179)
Exact Changes
Step 1: Read through the DEMO_ACCOUNTS object and compare each account's role and displayName against the actual roles in the RCME seed data (mmp/auth-service/scripts/seed-rcme-sandbox.ts).
Step 2: If any labels are out of sync, update the displayName values to match. Also add a clarifying comment:
/**
- Demo account configurations for Quick Login buttons.
- NOTE: These are static display labels for development convenience.
- Actual user roles are fetched from the server after login via refreshSession().
- If roles are changed via the admin panel, these labels won't auto-update.
*/
const DEMO_ACCOUNTS = { ... };
Step 3: If any specific role/displayName mismatches are found, fix them. Common issues:
- Display name says "Admin" but user's actual primary role is "Officer"
- Display name says "Member" but user has been promoted to an admin role
Verification
- Compare Quick Login button labels with actual user roles in the database
- Labels should match the default seed data roles (not runtime changes)
- This is a cosmetic fix — no functional changes to login behavior
Execution Order
- BUG-PHASE7-003 (S, ~10 min) — Fix "undefined" org name. No backend changes.
- BUG-PHASE3-002 (S, ~15 min) — Add missing role labels. No backend changes.
- BUG-PHASE3-001 (S, ~10 min) — Sync Quick Login labels. No backend changes.
- BUG-PHASE7-004 (M-L, ~2-3 hrs) — Add membership type field. Schema migration + backend + frontend. Must wait for Phase 1 BUG-PHASE7-002 to be fixed first.
Bugs 1-3 can be done in parallel. Bug 4 must wait for Phase 1 completion.
Docker Commands
# Run Prisma migration for BUG-PHASE7-004:
docker exec membervu-backend npx prisma migrate dev --name add-preferred-membership-type
Restart backend:
docker restart membervu-backend
Check logs:
docker logs membervu-backend --tail 50
Frontend hot-reloads automatically