Ctrl K
ComunidadGuías…

specs/010-brief-ordering/tasks.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Brief Ordering Implementation Tasks

## Phase 1: Backend - Database and Entity Layer

**Goal**: Add position column to database and entity, with migration backfilling existing data.

### Tasks

1. **Create database migration `V21__add_brief_position.sql`**
   - Add nullable `position` column to `day_briefs` table
   - Backfill positions using `ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at)`
   - Set column to NOT NULL after backfill
   - Create index `idx_day_briefs_user_position` on `(user_id, position)`

2. **Update `DayBrief` entity**
   - Add `position` field with `@Column(nullable = false)`
   - No default value needed (handled by migration and service)

3. **Update `DayBriefDTO`**
   - Add `position` field (Integer)

4. **Update `DayBriefService.mapToDTO()`**
   - Include position in DTO mapping

**Verification**: Run `mvn test` - existing tests should pass. Verify migration applies cleanly with `mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=local"`.

---

## Phase 2: Backend - Repository and Service Updates

**Goal**: Briefs returned ordered by position; new briefs get position at end.

### Tasks

1. **Add repository methods to `DayBriefRepository`**
   - `Page<DayBrief> findByUserIdOrderByPositionAsc(UUID userId, Pageable pageable)`
   - `Page<DayBrief> findByUserIdAndStatusOrderByPositionAsc(UUID userId, DayBriefStatus status, Pageable pageable)`
   - `@Query("SELECT COALESCE(MAX(d.position), -1) FROM DayBrief d WHERE d.userId = :userId") Integer findMaxPositionByUserId(@Param("userId") UUID userId)`
   - `List<DayBrief> findByUserId(UUID userId)` (for reorder operation)

2. **Update `DayBriefService.listDayBriefs()`**
   - Use `findByUserIdOrderByPositionAsc` instead of `findByUserId`
   - Use `findByUserIdAndStatusOrderByPositionAsc` instead of `findByUserIdAndStatus`

3. **Update `DayBriefService.createDayBrief()`**
   - Get max position: `findMaxPositionByUserId(userId)`
   - Set `position = maxPosition + 1` on new brief

4. **Add unit tests in `DayBriefServiceTest`**
   - `createDayBrief_setsPositionAtEnd`
   - `listDayBriefs_orderedByPosition`

**Verification**: Run `mvn test`. Create 3 briefs via API, verify they return in creation order (positions 0, 1, 2).

---

## Phase 3: Backend - Reorder Endpoint

**Goal**: POST `/daybriefs/reorder` endpoint updates positions.

### Tasks

1. **Create `ReorderBriefsRequest` DTO**
   - Field: `List<UUID> briefIds` with `@NotNull` and `@Size(min = 1)`
   - Location: `be.transcode.morningdeck.server.core.dto`

2. **Add `DayBriefService.reorderBriefs()` method**
   ```java
   @Transactional
   public void reorderBriefs(UUID userId, List<UUID> briefIds) {
       List<DayBrief> briefs = dayBriefRepository.findByUserId(userId);
       if (briefs.size() != briefIds.size()) {
           throw new IllegalArgumentException("Request must contain all briefs");
       }
       Map<UUID, DayBrief> briefMap = briefs.stream()
           .collect(Collectors.toMap(DayBrief::getId, Function.identity()));
       for (int i = 0; i < briefIds.size(); i++) {
           DayBrief brief = briefMap.get(briefIds.get(i));
           if (brief == null) {
               throw new IllegalArgumentException("Invalid brief ID: " + briefIds.get(i));
           }
           brief.setPosition(i);
       }
       dayBriefRepository.saveAll(briefs);
       log.info("Reordered {} briefs for user {}", briefIds.size(), userId);
   }
   ```

3. **Add endpoint in `DayBriefController`**
   ```java
   @PostMapping("/reorder")
   public ResponseEntity<Void> reorderBriefs(
       @AuthenticationPrincipal User user,
       @Valid @RequestBody ReorderBriefsRequest request
   ) {
       dayBriefService.reorderBriefs(user.getId(), request.getBriefIds());
       return ResponseEntity.noContent().build();
   }
   ```

4. **Add unit tests in `DayBriefServiceTest`**
   - `reorderBriefs_validIds_updatesPositions`
   - `reorderBriefs_missingBriefId_throwsException`
   - `reorderBriefs_invalidBriefId_throwsException`

5. **Add integration tests in `BriefingCoreIT`**
   - `reorderBriefs_returnsNoContent`
   - `reorderBriefs_persistsNewOrder`
   - `reorderBriefs_otherUsersBrief_returns400`

**Verification**: Run `mvn test`. Test via curl:
```bash
curl -X POST http://localhost:3000/daybriefs/reorder \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"briefIds": ["uuid3", "uuid1", "uuid2"]}'
```

---

## Phase 4: Frontend - Dependencies and Types

**Goal**: Install @dnd-kit and update TypeScript types.

### Tasks

1. **Install dependencies**
   ```bash
   cd frontend && npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
   ```

2. **Update `src/types/index.ts`**
   - Add `position: number` to `DayBriefDTO` interface

3. **Add `reorderBriefs` method to `src/lib/api.ts`**
   ```typescript
   async reorderBriefs(briefIds: string[]): Promise<void> {
     return this.request('/daybriefs/reorder', {
       method: 'POST',
       body: JSON.stringify({ briefIds }),
     });
   }
   ```

**Verification**: Run `npm run build` - should compile without errors.

---

## Phase 5: Frontend - Drag and Drop UI

**Goal**: Implement sortable briefs list with optimistic updates.

### Tasks

1. **Create `SortableBriefItem.tsx` component**
   - Location: `src/components/layout/SortableBriefItem.tsx`
   - Use `useSortable` hook from `@dnd-kit/sortable`
   - Apply transform/transition styles
   - Reduce opacity when dragging
   - Forward ref to `SidebarMenuItem`

2. **Update `SidebarBriefsList.tsx`**
   - Import DnD components: `DndContext`, `SortableContext`, `closestCenter`, sensors
   - Configure sensors: `PointerSensor` (distance: 8), `KeyboardSensor`
   - Wrap `SidebarMenu` with `DndContext` and `SortableContext`
   - Use `verticalListSortingStrategy`
   - Implement `handleDragEnd`:
     - Optimistic update with `arrayMove`
     - Call `apiClient.reorderBriefs()`
     - Revert on error with toast notification
   - Disable drag during loading state
   - Replace inline brief rendering with `SortableBriefItem`

3. **Update `handleCreateSuccess` in `SidebarBriefsList.tsx`**
   - Append new brief to end of list (not prepend)
   - Change `[brief, ...prev]` to `[...prev, brief]`

**Verification**:
1. Run `npm run dev`
2. Create 3 briefs
3. Drag to reorder
4. Refresh page - order should persist
5. Disconnect network, try to reorder - should revert with error toast

---

## Phase 6: Polish and Edge Cases

**Goal**: Handle edge cases and improve UX.

### Tasks

1. **Touch device support**
   - Add `TouchSensor` to sensors array
   - Configure with `activationConstraint: { delay: 250, tolerance: 5 }`

2. **Keyboard accessibility**
   - Verify arrow keys work for reordering (provided by `KeyboardSensor` + `sortableKeyboardCoordinates`)

3. **Visual feedback improvements**
   - Add `cursor: grab` on hover, `cursor: grabbing` when dragging
   - Optional: Add subtle drag handle icon (GripVertical from lucide-react)

4. **Handle concurrent modifications**
   - If reorder fails with 400 (brief count mismatch), emit `BRIEFS_CHANGED` to refresh list

**Verification**:
1. Test on mobile device or Chrome DevTools mobile emulation
2. Test keyboard navigation with Tab + Enter + Arrow keys
3. Delete a brief in another tab, try reordering - should refresh and show updated list