hensei-web/src/lib/services/grid.service.ts
Justin Edmund b54ac91638 fix: correct type comparison errors
- jobUtils.ts: Remove string comparison for job.row (row is typed as number)
  - job.row === '1' comparison is always false, removed
- grid.service.ts: Fix swap operation to compare position with position
  - Changed i.id === operation.targetPosition to i.position === operation.targetPosition
  - targetPosition is a number (position), not a string (id)

Fixes "This comparison appears to be unintentional because the types have no overlap" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 18:26:18 -08:00

598 lines
15 KiB
TypeScript

import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
import { gridAdapter } from '$lib/api/adapters/grid.adapter'
import { partyAdapter } from '$lib/api/adapters/party.adapter'
export interface GridOperation {
type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
itemId?: string
position?: number
targetPosition?: number
uncapLevel?: number
transcendenceLevel?: number
data?: any
}
export interface GridUpdateResult {
party: Party
conflicts?: {
conflicts: string[]
incoming: string
position: number
}
}
/**
* Grid service - handles grid operations for weapons, summons, and characters
*/
export class GridService {
constructor() {}
// Weapon Grid Operations
async addWeapon(
partyId: string,
weaponId: string,
position: number,
editKey?: string,
options?: { mainhand?: boolean; shortcode?: string }
): Promise<GridUpdateResult> {
try {
// Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags
const gridWeapon = await gridAdapter.createWeapon({
partyId,
weaponId,
position,
mainhand: options?.mainhand,
transcendenceStep: 0
}, this.buildHeaders(editKey))
console.log('[GridService] Created grid weapon:', gridWeapon)
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Return success without fetching party - the caller should refresh if needed
// partyId is a UUID, not a shortcode, so we can't fetch here
return { party: null as any }
} catch (error: any) {
console.error('[GridService] Error creating weapon:', error)
if (error.type === 'conflict') {
return {
party: null as any, // Will be handled by conflict resolution
conflicts: error
}
}
throw error
}
}
async replaceWeapon(
partyId: string,
gridWeaponId: string,
newWeaponId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<GridUpdateResult> {
try {
// First remove the old weapon
await gridAdapter.deleteWeapon({ id: gridWeaponId, partyId }, this.buildHeaders(editKey))
// Then add the new one (pass shortcode along)
const result = await this.addWeapon(partyId, newWeaponId, 0, editKey, options)
return result
} catch (error: any) {
if (error.type === 'conflict') {
return {
party: null as any,
conflicts: error
}
}
throw error
}
}
async removeWeapon(
partyId: string,
gridWeaponId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.deleteWeapon({ id: gridWeaponId, partyId }, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateWeapon(
partyId: string,
gridWeaponId: string,
updates: {
position?: number
uncapLevel?: number
transcendenceStep?: number
element?: number
},
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.updateWeapon(gridWeaponId, {
position: updates.position,
uncapLevel: updates.uncapLevel,
transcendenceStep: updates.transcendenceStep,
element: updates.element
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async moveWeapon(
partyId: string,
gridWeaponId: string,
newPosition: number,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.updateWeaponPosition({
partyId,
id: gridWeaponId,
position: newPosition
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async swapWeapons(
partyId: string,
gridWeaponId1: string,
gridWeaponId2: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.swapWeapons({
partyId,
sourceId: gridWeaponId1,
targetId: gridWeaponId2
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateWeaponUncap(
partyId: string,
gridWeaponId: string,
uncapLevel?: number,
transcendenceStep?: number,
editKey?: string
): Promise<any> {
return gridAdapter.updateWeaponUncap({
id: gridWeaponId,
partyId,
uncapLevel: uncapLevel ?? 3,
transcendenceStep
}, this.buildHeaders(editKey))
}
// Summon Grid Operations
async addSummon(
partyId: string,
summonId: string,
position: number,
editKey?: string,
options?: { main?: boolean; friend?: boolean; shortcode?: string }
): Promise<Party> {
// Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags
const gridSummon = await gridAdapter.createSummon({
partyId,
summonId,
position,
main: options?.main,
friend: options?.friend,
transcendenceStage: 0
}, this.buildHeaders(editKey))
console.log('[GridService] Created grid summon:', gridSummon)
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - partyId is UUID not shortcode
return null as any
}
async replaceSummon(
partyId: string,
gridSummonId: string,
newSummonId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party> {
// First remove the old summon
await gridAdapter.deleteSummon({ id: gridSummonId, partyId }, this.buildHeaders(editKey))
// Then add the new one (pass shortcode along)
return this.addSummon(partyId, newSummonId, 0, editKey, { ...options })
}
async removeSummon(
partyId: string,
gridSummonId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.deleteSummon({ id: gridSummonId, partyId }, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateSummon(
partyId: string,
gridSummonId: string,
updates: {
position?: number
quickSummon?: boolean
uncapLevel?: number
transcendenceStep?: number
},
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.updateSummon(gridSummonId, {
position: updates.position,
quickSummon: updates.quickSummon,
uncapLevel: updates.uncapLevel,
transcendenceStage: updates.transcendenceStep
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async moveSummon(
partyId: string,
gridSummonId: string,
newPosition: number,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.updateSummonPosition({
partyId,
id: gridSummonId,
position: newPosition
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async swapSummons(
partyId: string,
gridSummonId1: string,
gridSummonId2: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.swapSummons({
partyId,
sourceId: gridSummonId1,
targetId: gridSummonId2
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateSummonUncap(
partyId: string,
gridSummonId: string,
uncapLevel?: number,
transcendenceStep?: number,
editKey?: string
): Promise<any> {
return gridAdapter.updateSummonUncap({
id: gridSummonId,
partyId,
uncapLevel: uncapLevel ?? 3,
transcendenceStep
}, this.buildHeaders(editKey))
}
// Character Grid Operations
async addCharacter(
partyId: string,
characterId: string,
position: number,
editKey?: string,
options?: { shortcode?: string }
): Promise<GridUpdateResult> {
try {
// Note: The backend computes the correct uncap level based on the character's special/FLB/ULB flags
const gridCharacter = await gridAdapter.createCharacter({
partyId,
characterId,
position,
transcendenceStep: 0
}, this.buildHeaders(editKey))
console.log('[GridService] Created grid character:', gridCharacter)
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - partyId is UUID not shortcode
return { party: null as any }
} catch (error: any) {
if (error.type === 'conflict') {
return {
party: null as any,
conflicts: error
}
}
throw error
}
}
async replaceCharacter(
partyId: string,
gridCharacterId: string,
newCharacterId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<GridUpdateResult> {
try {
// First remove the old character
await gridAdapter.deleteCharacter({ id: gridCharacterId, partyId }, this.buildHeaders(editKey))
// Then add the new one (pass shortcode along)
return this.addCharacter(partyId, newCharacterId, 0, editKey, options)
} catch (error: any) {
if (error.type === 'conflict') {
return {
party: null as any,
conflicts: error
}
}
throw error
}
}
async removeCharacter(
partyId: string,
gridCharacterId: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.deleteCharacter({ id: gridCharacterId, partyId }, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateCharacter(
partyId: string,
gridCharacterId: string,
updates: {
position?: number
uncapLevel?: number
transcendenceStep?: number
perpetuity?: boolean
},
editKey?: string,
options?: { shortcode?: string }
): Promise<GridCharacter | null> {
const updated = await gridAdapter.updateCharacter(gridCharacterId, {
position: updates.position,
uncapLevel: updates.uncapLevel,
transcendenceStep: updates.transcendenceStep,
perpetuity: updates.perpetuity
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Return the updated character
return updated
}
async moveCharacter(
partyId: string,
gridCharacterId: string,
newPosition: number,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.updateCharacterPosition({
partyId,
id: gridCharacterId,
position: newPosition
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async swapCharacters(
partyId: string,
gridCharacterId1: string,
gridCharacterId2: string,
editKey?: string,
options?: { shortcode?: string }
): Promise<Party | null> {
await gridAdapter.swapCharacters({
partyId,
sourceId: gridCharacterId1,
targetId: gridCharacterId2
}, this.buildHeaders(editKey))
// Clear party cache if shortcode provided
if (options?.shortcode) {
partyAdapter.clearPartyCache(options.shortcode)
}
// Don't fetch - let caller handle refresh
return null
}
async updateCharacterUncap(
partyId: string,
gridCharacterId: string,
uncapLevel?: number,
transcendenceStep?: number,
editKey?: string
): Promise<any> {
return gridAdapter.updateCharacterUncap({
id: gridCharacterId,
partyId,
uncapLevel: uncapLevel ?? 3,
transcendenceStep
}, this.buildHeaders(editKey))
}
// Drag and Drop Helpers
/**
* Normalize drag and drop intent to a grid operation
*/
normalizeDragIntent(
dragType: 'weapon' | 'summon' | 'character',
draggedItem: any,
targetPosition: number,
targetItem?: any
): GridOperation {
// If dropping on an empty slot
if (!targetItem) {
return {
type: 'add',
itemId: draggedItem.id,
position: targetPosition
}
}
// If dragging from grid to grid
if (draggedItem.gridId && targetItem.gridId) {
return {
type: 'swap',
itemId: draggedItem.gridId,
targetPosition: targetItem.gridId
}
}
// If dragging from outside to occupied slot
return {
type: 'replace',
itemId: targetItem.gridId,
targetPosition: draggedItem.id
}
}
/**
* Apply optimistic update to local state
*/
applyOptimisticUpdate<T extends GridWeapon | GridSummon | GridCharacter>(
items: T[],
operation: GridOperation
): T[] {
const updated = [...items]
switch (operation.type) {
case 'add':
// Add new item at position
break
case 'remove':
return updated.filter(item => item.id !== operation.itemId)
case 'move':
const item = updated.find(i => i.id === operation.itemId)
if (item && operation.targetPosition !== undefined) {
item.position = operation.targetPosition
}
break
case 'swap':
const item1 = updated.find(i => i.id === operation.itemId)
const item2 = updated.find(i => i.position === operation.targetPosition)
if (item1 && item2) {
const tempPos = item1.position
item1.position = item2.position
item2.position = tempPos
}
break
}
return updated
}
// Private helpers
private buildHeaders(editKey?: string): Record<string, string> {
const headers: Record<string, string> = {}
if (editKey) {
headers['X-Edit-Key'] = editKey
}
return headers
}
}