From 0599db101fa6b1dde9fa138eb9ffc945d3e1eadc Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 22:52:23 -0800 Subject: [PATCH] update crew plan with Phase 1 completion --- docs/plans/crew-feature.md | 536 +++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 docs/plans/crew-feature.md diff --git a/docs/plans/crew-feature.md b/docs/plans/crew-feature.md new file mode 100644 index 0000000..0a0bd3f --- /dev/null +++ b/docs/plans/crew-feature.md @@ -0,0 +1,536 @@ +# Crew Feature Implementation Plan + +## Overview + +Implement a comprehensive Crew system for Granblue Fantasy team management, including crew formation, role management, invitations, Unite and Fight (GW) event tracking with individual and crew-wide scoring, and interactive visualizations. + +## Workflow + +- Commit often in logical groups +- Track commit hashes next to completed tasks: `[abc1234]` +- Terse, informal commit messages + +--- + +## Phase 1: Core Crew Infrastructure + +### Database Migrations + +- [x] **1. `create_crews`** `[9b01aa0]` +``` +crews: + - id: uuid (PK) + - name: string (required, max 100) + - gamertag: string (text-only tag for profiles) + - granblue_crew_id: string (in-game ID, unique) + - description: text + - timestamps +``` + +- [x] **2. `create_crew_memberships`** `[9b01aa0]` +``` +crew_memberships: + - id: uuid (PK) + - crew_id: uuid (FK, required) + - user_id: uuid (FK, required) + - role: integer (0=member, 1=vice_captain, 2=captain) + - retired: boolean (default: false) + - retired_at: datetime + - timestamps + +Indices: + - unique on [crew_id, user_id] + - partial unique on [user_id] WHERE retired=false (one active crew per user) +``` + +- [x] **3. `add_show_gamertag_to_users`** `[9b01aa0]` +``` +Add: show_gamertag: boolean (default: true) +``` + +### Backend Files to Create + +| Status | File | Purpose | +|:------:|------|---------| +| [x] `[9b01aa0]` | `app/models/crew.rb` | Crew model with associations, validations | +| [x] `[9b01aa0]` | `app/models/crew_membership.rb` | Membership with role enum, retirement logic | +| [x] `[e98e594]` | `app/controllers/api/v1/crews_controller.rb` | CRUD, leave, transfer captain | +| [x] `[e98e594]` | `app/controllers/api/v1/crew_memberships_controller.rb` | Promote, demote, remove | +| [x] `[e98e594]` | `app/controllers/concerns/crew_authorization_concern.rb` | Officer/captain checks | +| [x] `[e98e594]` | `app/blueprints/api/v1/crew_blueprint.rb` | Crew serialization | +| [x] `[e98e594]` | `app/blueprints/api/v1/crew_membership_blueprint.rb` | Membership serialization | +| [x] `[872b6fd]` | `app/errors/crew_errors.rb` | Custom errors (restructured for Zeitwerk) | + +### Backend Files to Modify + +| Status | File | Changes | +|:------:|------|---------| +| [x] `[9b01aa0]` | `app/models/user.rb` | Add crew associations, implement `in_same_crew_as?` | +| [x] `[9b01aa0]` | `app/models/user.rb` | Update `collection_viewable_by?` for crew_only | +| [ ] | `app/blueprints/api/v1/user_blueprint.rb` | Add gamertag and crew info to minimal view | +| [x] `[e98e594]` | `config/routes.rb` | Add crew routes | + +### Backend Tests + +| Status | File | Purpose | +|:------:|------|---------| +| [x] `[872b6fd]` | `spec/models/crew_spec.rb` | Crew model specs (18 examples) | +| [x] `[872b6fd]` | `spec/models/crew_membership_spec.rb` | CrewMembership model specs (16 examples) | +| [x] `[872b6fd]` | `spec/models/user_spec.rb` | User crew association specs (22 examples) | +| [x] `[872b6fd]` | `spec/requests/crews_controller_spec.rb` | Crews API specs (15 examples) | +| [x] `[872b6fd]` | `spec/requests/crew_memberships_controller_spec.rb` | Memberships API specs (13 examples) | +| [x] `[872b6fd]` | `spec/factories/crews.rb` | Crew factory | +| [x] `[872b6fd]` | `spec/factories/crew_memberships.rb` | CrewMembership factory | + +### API Endpoints + +``` +POST /crews - Create crew (user becomes captain) +GET /crew - Current user's crew +PUT /crew - Update crew (officers only) +GET /crew/members - List members +POST /crew/leave - Leave crew (not captain) +POST /crews/:id/transfer_captain - Transfer ownership +PUT /crews/:id/memberships/:id - Update member role +DELETE /crews/:id/memberships/:id - Remove member +``` + +### Frontend (hensei-web) + +**Pages:** +- [ ] `/crew` - Dashboard (redirect to create if no crew) +- [ ] `/crew/create` - Create crew form +- [ ] `/crew/settings` - Crew settings (officers) +- [ ] `/crew/members` - Member management + +**Components:** +- [ ] `components/crew/CrewDashboard/` +- [ ] `components/crew/CrewHeader/` +- [ ] `components/crew/CrewMemberList/` +- [ ] `components/crew/CreateCrewForm/` + +--- + +## Phase 2: Invitations System + +### Database Migration + +- [ ] **`create_crew_invitations`** +``` +crew_invitations: + - id: uuid (PK) + - crew_id: uuid (FK, required) + - user_id: uuid (FK, required - invitee) + - invited_by_id: uuid (FK, required) + - status: integer (0=pending, 1=accepted, 2=rejected, 3=expired) + - expires_at: datetime + - timestamps + +Indices: + - on [crew_id, user_id, status] + - on [user_id, status] +``` + +### Backend Files to Create + +| Status | File | Purpose | +|:------:|------|---------| +| [ ] | `app/models/crew_invitation.rb` | Invitation model with accept/reject | +| [ ] | `app/controllers/api/v1/crew_invitations_controller.rb` | Send, list, accept, reject | + +### API Endpoints + +``` +POST /crews/:id/invitations - Send invitation (officers) +GET /crews/:id/invitations - List crew's invitations +GET /invitations/pending - User's pending invitations +POST /invitations/:id/accept - Accept invitation +POST /invitations/:id/reject - Reject invitation +``` + +### Frontend + +**Pages:** +- [ ] `/crew/join` - View and respond to invitations + +**Components:** +- [ ] `components/crew/CrewInvitationBanner/` - Top-of-page banner in Header +- [ ] `components/crew/JoinCrewSection/` - Invitation list with accept/reject + +**Modify:** +- [ ] `components/Header/` - Add invitation banner for pending invites + +--- + +## Phase 3: GW Events & Basic Scoring + +### Database Migrations + +- [ ] **1. `create_gw_events`** (admin-managed via Database CMS) +``` +gw_events: + - id: uuid (PK) + - name: string (required) + - element: integer (required, uses GranblueEnums::ELEMENTS) + - start_date: date (required) + - end_date: date (required) + - event_number: integer (GW #XX, unique) + - timestamps +``` + +- [ ] **2. `create_crew_gw_participations`** +``` +crew_gw_participations: + - id: uuid (PK) + - crew_id: uuid (FK) + - gw_event_id: uuid (FK) + - preliminary_ranking: bigint + - final_ranking: bigint + - timestamps + +Unique: [crew_id, gw_event_id] +``` + +- [ ] **3. `create_gw_crew_scores`** (crew-wide round scores) +``` +gw_crew_scores: + - id: uuid (PK) + - crew_gw_participation_id: uuid (FK) + - round: integer (0=prelims, 1=interlude, 2-5=finals day 1-4) + - crew_score: bigint + - opponent_score: bigint + - opponent_name: string + - opponent_granblue_id: string + - victory: boolean + - timestamps + +Unique: [crew_gw_participation_id, round] +``` + +- [ ] **4. `create_gw_individual_scores`** +``` +gw_individual_scores: + - id: uuid (PK) + - crew_gw_participation_id: uuid (FK) + - crew_membership_id: uuid (FK, nullable) + - phantom_player_id: uuid (FK, nullable) + - round: integer + - score: bigint (default: 0) + - is_cumulative: boolean (default: false) + - recorded_by_id: uuid (FK) + - timestamps + +Constraints: Exactly one of crew_membership_id or phantom_player_id must be set +``` + +### Backend Files to Create + +| Status | File | Purpose | +|:------:|------|---------| +| [ ] | `app/models/gw_event.rb` | Event model with element, dates | +| [ ] | `app/models/crew_gw_participation.rb` | Links crew to event | +| [ ] | `app/models/gw_crew_score.rb` | Crew-level round scores | +| [ ] | `app/models/gw_individual_score.rb` | Individual scores with permission checks | +| [ ] | `app/controllers/api/v1/gw_events_controller.rb` | CRUD (admin create/update) | +| [ ] | `app/controllers/api/v1/crew_gw_participations_controller.rb` | Join event, get participation | +| [ ] | `app/controllers/api/v1/gw_crew_scores_controller.rb` | Crew score entry | +| [ ] | `app/controllers/api/v1/gw_individual_scores_controller.rb` | Individual + batch entry | +| [ ] | `app/blueprints/api/v1/gw_*_blueprint.rb` | Serializers for all GW models | + +### API Endpoints + +``` +# GW Events (admin creates via CMS) +GET /gw_events - List all events +GET /gw_events/:id - Show event +POST /gw_events - Create event (admin) +PUT /gw_events/:id - Update event (admin) + +# Participations +POST /gw_events/:id/participations - Join event +GET /crew/gw_participations - Crew's participations +GET /crew/gw_participations/:id - Single participation with scores + +# Scores +POST /gw_events/:eid/participations/:pid/crew_scores - Add crew score +PUT /gw_events/:eid/participations/:pid/crew_scores/:id - Update crew score +POST /gw_events/:eid/participations/:pid/individual_scores - Add individual +POST /gw_events/:eid/participations/:pid/individual_scores/batch - Batch entry +``` + +### Frontend + +**Pages:** +- [ ] `/crew/gw` - GW overview (list of events) +- [ ] `/crew/gw/[eventId]` - Event detail with scores +- [ ] `/crew/gw/[eventId]/scores` - Score entry interface + +**Components:** +- [ ] `components/gw/GwEventCard/` +- [ ] `components/gw/GwScoreBoard/` +- [ ] `components/gw/GwCrewScoreForm/` +- [ ] `components/gw/GwIndividualScoreForm/` + +--- + +## Phase 4: Phantom Players & Retired Members + +### Database Migration + +- [ ] **`create_phantom_players`** +``` +phantom_players: + - id: uuid (PK) + - crew_id: uuid (FK) + - name: string (required) + - granblue_id: string + - notes: text + - claimed_by_id: uuid (FK to users, nullable) + - claimed_from_membership_id: uuid (FK, nullable) + - claim_confirmed: boolean (default: false) + - timestamps + +Unique: [crew_id, granblue_id] WHERE granblue_id IS NOT NULL +``` + +### Backend Files to Create + +| Status | File | Purpose | +|:------:|------|---------| +| [ ] | `app/models/phantom_player.rb` | Phantom with claim flow | +| [ ] | `app/controllers/api/v1/phantom_players_controller.rb` | CRUD, assign, confirm | + +### Claim Flow + +1. Captain creates phantom player (name, granblue_id, notes) +2. Real user joins crew +3. Captain assigns phantom to user (`claimed_by_id` set) +4. User confirms claim (`claim_confirmed` = true) +5. All phantom scores transfer to user's membership + +### Retirement Flow + +1. Member leaves or is removed +2. `CrewMembership.retired` = true, `retired_at` = now +3. Historical scores remain linked to membership +4. No new scores can be added to retired members +5. Scores visible to current crew members + +### API Endpoints + +``` +GET /crews/:id/phantom_players - List phantoms +POST /crews/:id/phantom_players - Create phantom +PUT /crews/:id/phantom_players/:id - Update phantom +DELETE /crews/:id/phantom_players/:id - Delete phantom +POST /crews/:id/phantom_players/:id/assign - Assign to user +POST /crews/:id/phantom_players/:id/confirm_claim - User confirms +``` + +### Frontend + +**Components:** +- [ ] `components/phantom/PhantomPlayerList/` +- [ ] `components/phantom/PhantomPlayerForm/` +- [ ] `components/phantom/PhantomClaimDialog/` + +--- + +## Phase 5: Batch Score Entry & Visualization + +### Backend + +- [ ] **Batch Endpoint Enhancement:** +``` +POST /gw_events/:eid/participations/:pid/individual_scores/batch +Body: { scores: [{ player_id, player_type, round, score }, ...] } +``` + +- [ ] **Aggregation Endpoints:** +``` +GET /crew/gw_participations/:id/leaderboard - Player rankings +GET /crew/gw_participations/:id/chart_data - Time series for charts +``` + +### Frontend - Batch Entry + +- [ ] **Component:** `components/gw/GwBatchScoreEntry/` + - Spreadsheet-style grid (players as rows, rounds as columns) + - Tab/Enter navigation + - Paste from spreadsheet support + - Toggle between individual and batch modes + +### Frontend - Visualization (Apache ECharts) + +**Install:** `npm install echarts echarts-for-react` + +**Components:** +- [ ] `components/gw/GwScoreChart/` - Main interactive chart +- [ ] `components/gw/GwPlayerRanking/` - Sortable player leaderboard +- [ ] `components/gw/GwProgressChart/` - Progress over time + +**Chart Features:** +- Individual focus: Player rankings, personal progress, personal bests +- Time axis control: Filter by event date range +- Player selection: Toggle which players are visible +- Event filter: Compare across multiple GW events + +--- + +## Phase 6: Profile Integration & Polish + +### Backend + +- [ ] **Update UserBlueprint** (`app/blueprints/api/v1/user_blueprint.rb`): +```ruby +view :minimal do + # ... existing fields ... + + field :gamertag, if: ->(_, user, _) { + user.show_gamertag && user.crew&.gamertag.present? + } do |user| + user.crew&.gamertag + end +end +``` + +### Frontend + +**User Settings:** +- [ ] Add toggle: "Show crew gamertag on profile" +- [ ] Located in existing settings page + +**Profile Display:** +- [ ] Show gamertag badge next to username when enabled +- [ ] Format: `[CREW] Username` or similar styling + +**Admin Pages:** +- [ ] `/admin/gw-events` - GW event management for Database CMS + +--- + +## Authorization Matrix + +| Action | Captain | Vice Captain | Member | Non-member | +|--------|:-------:|:------------:|:------:|:----------:| +| View crew dashboard | Yes | Yes | Yes | No | +| Edit crew info | Yes | Yes | No | No | +| Send invitations | Yes | Yes | No | No | +| Promote to VC | Yes | No | No | No | +| Demote VC | Yes | No | No | No | +| Remove member | Yes | Yes | No | No | +| Transfer captain | Yes | No | No | No | +| Record own score | Yes | Yes | Yes | No | +| Record others' scores | Yes | Yes | No | No | +| Manage phantoms | Yes | Yes | No | No | +| View GW history | Yes | Yes | Yes | No | + +--- + +## Key Files Reference + +### Backend - To Create +``` +app/models/ + crew.rb + crew_membership.rb + crew_invitation.rb + phantom_player.rb + gw_event.rb + crew_gw_participation.rb + gw_crew_score.rb + gw_individual_score.rb + +app/controllers/api/v1/ + crews_controller.rb + crew_memberships_controller.rb + crew_invitations_controller.rb + phantom_players_controller.rb + gw_events_controller.rb + crew_gw_participations_controller.rb + gw_crew_scores_controller.rb + gw_individual_scores_controller.rb + +app/blueprints/api/v1/ + crew_blueprint.rb + crew_membership_blueprint.rb + crew_invitation_blueprint.rb + phantom_player_blueprint.rb + gw_event_blueprint.rb + crew_gw_participation_blueprint.rb + gw_crew_score_blueprint.rb + gw_individual_score_blueprint.rb + +app/controllers/concerns/ + crew_authorization_concern.rb + +app/errors/ + crew_errors.rb +``` + +### Backend - To Modify +``` +app/models/user.rb # Add crew associations +app/blueprints/api/v1/user_blueprint.rb # Add gamertag +config/routes.rb # Add all crew routes +``` + +### Frontend - To Create +``` +app/[locale]/crew/ + page.tsx + create/page.tsx + settings/page.tsx + members/page.tsx + join/page.tsx + gw/ + page.tsx + [eventId]/ + page.tsx + scores/page.tsx + +components/ + crew/ + CrewDashboard/ + CrewHeader/ + CrewMemberList/ + CrewInvitationBanner/ + CreateCrewForm/ + JoinCrewSection/ + gw/ + GwEventCard/ + GwScoreBoard/ + GwCrewScoreForm/ + GwIndividualScoreForm/ + GwBatchScoreEntry/ + GwScoreChart/ + GwPlayerRanking/ + GwProgressChart/ + phantom/ + PhantomPlayerList/ + PhantomPlayerForm/ + PhantomClaimDialog/ + +stores/ + crewStore.ts + gwStore.ts +``` + +### Frontend - To Modify +``` +components/Header/index.tsx # Add invitation banner +app/[locale]/settings/ # Add gamertag toggle +``` + +--- + +## Implementation Order + +1. **Phase 1** - Core crew (can deploy independently) +2. **Phase 2** - Invitations (enables recruiting) +3. **Phase 3** - GW events & scoring (core value feature) +4. **Phase 4** - Phantom players (completes scoring for non-users) +5. **Phase 5** - Batch entry & visualization (polish & UX) +6. **Phase 6** - Profile integration (final touches) + +Each phase is deployable independently after Phase 1.