546 lines
16 KiB
Markdown
546 lines
16 KiB
Markdown
# 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
|
|
|
|
- [x] **`create_crew_invitations`** `[b75a905]`
|
|
```
|
|
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 |
|
|
|:------:|------|---------|
|
|
| [x] `[b75a905]` | `app/models/crew_invitation.rb` | Invitation model with accept/reject |
|
|
| [x] `[b75a905]` | `app/controllers/api/v1/crew_invitations_controller.rb` | Send, list, accept, reject |
|
|
| [x] `[b75a905]` | `app/blueprints/api/v1/crew_invitation_blueprint.rb` | Invitation serialization |
|
|
| [x] `[b75a905]` | `app/errors/crew_errors.rb` | Added invitation error classes |
|
|
|
|
### Backend Tests
|
|
|
|
| Status | File | Purpose |
|
|
|:------:|------|---------|
|
|
| [x] `[b75a905]` | `spec/models/crew_invitation_spec.rb` | CrewInvitation model specs (21 examples) |
|
|
| [x] `[b75a905]` | `spec/requests/crew_invitations_controller_spec.rb` | Invitations API specs (17 examples) |
|
|
| [x] `[b75a905]` | `spec/factories/crew_invitations.rb` | CrewInvitation factory |
|
|
|
|
### 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.
|