hensei-web/src/lib/services/party.service.ts

295 lines
8.5 KiB
TypeScript

import type { Party } from '$lib/types/api/party'
import { partyAdapter } from '$lib/api/adapters'
import { authStore } from '$lib/stores/auth.store'
import { browser } from '$app/environment'
export interface EditabilityResult {
canEdit: boolean
headers?: Record<string, string>
reason?: string
}
export interface PartyUpdatePayload {
name?: string | null
description?: string | null
element?: number
raidId?: string
chargeAttack?: boolean
fullAuto?: boolean
autoGuard?: boolean
autoSummon?: boolean
clearTime?: number | null
buttonCount?: number | null
chainCount?: number | null
turnCount?: number | null
jobId?: string
visibility?: number
localId?: string
}
/**
* Party service - handles business logic for party operations
*/
export class PartyService {
constructor() {}
/**
* Get party by shortcode
*/
async getByShortcode(shortcode: string): Promise<Party> {
return partyAdapter.getByShortcode(shortcode)
}
/**
* Clear party cache for a specific shortcode
*/
clearPartyCache(shortcode: string): void {
partyAdapter.clearPartyCache(shortcode)
}
/**
* Create a new party
*/
async create(payload: PartyUpdatePayload, editKey?: string): Promise<{
party: Party
editKey?: string
}> {
const headers = this.buildHeaders(editKey)
const apiPayload = this.mapToApiPayload(payload)
const party = await partyAdapter.create(apiPayload, headers)
// Note: Edit key handling may need to be adjusted based on how the API returns it
return { party, editKey: undefined }
}
/**
* Update party details
*/
async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
const headers = this.buildHeaders(editKey)
const apiPayload = this.mapToApiPayload(payload)
return partyAdapter.update({ shortcode: id, ...apiPayload }, headers)
}
/**
* Update party guidebooks
*/
async updateGuidebooks(
id: string,
position: number,
guidebookId: string | null,
editKey?: string
): Promise<Party> {
const headers = this.buildHeaders(editKey)
const payload: any = {}
// Map position to guidebook1_id, guidebook2_id, guidebook3_id
if (position >= 0 && position <= 2) {
payload[`guidebook${position + 1}Id`] = guidebookId
}
return partyAdapter.update({ shortcode: id, ...payload }, headers)
}
/**
* Remix a party (create a copy)
*/
async remix(shortcode: string, localId?: string, editKey?: string): Promise<{
party: Party
editKey?: string
}> {
const headers = this.buildHeaders(editKey)
const party = await partyAdapter.remix(shortcode, headers)
// Note: Edit key handling may need to be adjusted
return { party, editKey: undefined }
}
/**
* Favorite a party
*/
async favorite(id: string): Promise<void> {
return partyAdapter.favorite(id)
}
/**
* Unfavorite a party
*/
async unfavorite(id: string): Promise<void> {
return partyAdapter.unfavorite(id)
}
/**
* Delete a party
*/
async delete(id: string, editKey?: string): Promise<void> {
// The API expects the party ID, not shortcode, for delete
// We need to make a direct request with the ID
const headers = this.buildHeaders(editKey)
// Get auth token from authStore
const authHeaders: Record<string, string> = {}
if (browser) {
const token = await authStore.checkAndRefresh()
if (token) {
authHeaders['Authorization'] = `Bearer ${token}`
}
}
const finalHeaders = {
'Content-Type': 'application/json',
...authHeaders,
...headers
}
const url = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'}/parties/${id}`
console.log('[PartyService] DELETE Request Details:', {
url,
method: 'DELETE',
headers: finalHeaders,
credentials: 'include',
partyId: id,
hasEditKey: !!editKey,
hasAuthToken: !!authHeaders['Authorization']
})
// Make direct API call since adapter expects shortcode but API needs ID
const response = await fetch(url, {
method: 'DELETE',
credentials: 'include',
headers: finalHeaders
})
console.log('[PartyService] DELETE Response:', {
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries())
})
if (!response.ok) {
// Try to parse error body for more details
let errorBody = null
try {
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
errorBody = await response.json()
} else {
errorBody = await response.text()
}
} catch (e) {
console.error('[PartyService] Could not parse error response body:', e)
}
console.error('[PartyService] DELETE Failed:', {
status: response.status,
statusText: response.statusText,
errorBody,
url,
partyId: id
})
throw new Error(`Failed to delete party: ${response.status} ${response.statusText}${errorBody ? ` - ${JSON.stringify(errorBody)}` : ''}`)
}
console.log('[PartyService] DELETE Success - Party deleted:', id)
}
/**
* Compute editability for a party
*/
computeEditability(
party: Party,
authUserId?: string,
localId?: string,
editKey?: string
): EditabilityResult {
// Owner can always edit
if (authUserId && party.user?.id === authUserId) {
return { canEdit: true, reason: 'owner' }
}
// Local owner can edit if no server user
const isLocalOwner = localId && party.localId === localId
const hasNoServerUser = !party.user?.id
if (isLocalOwner && hasNoServerUser) {
const base = { canEdit: true, reason: 'local_owner' as const }
return editKey ? { ...base, headers: { 'X-Edit-Key': editKey } } : base
}
// Check for edit key permission
if (editKey && typeof window !== 'undefined') {
const storedKey = localStorage.getItem(`edit_key_${party.shortcode}`)
if (storedKey === editKey) {
return { canEdit: true, headers: { 'X-Edit-Key': editKey }, reason: 'edit_key' }
}
}
return { canEdit: false, reason: 'no_permission' }
}
/**
* Get or create local ID for anonymous users
*/
getLocalId(): string {
if (typeof window === 'undefined') return ''
let localId = localStorage.getItem('local_id')
if (!localId) {
localId = crypto.randomUUID()
localStorage.setItem('local_id', localId)
}
return localId
}
/**
* Get edit key for a party
*/
getEditKey(shortcode: string): string | null {
if (typeof window === 'undefined') return null
return localStorage.getItem(`edit_key_${shortcode}`)
}
/**
* Store edit key for a party
*/
storeEditKey(shortcode: string, editKey: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem(`edit_key_${shortcode}`, editKey)
}
}
// Private helpers
private buildHeaders(editKey?: string): Record<string, string> {
const headers: Record<string, string> = {}
if (editKey) {
headers['X-Edit-Key'] = editKey
}
return headers
}
private mapToApiPayload(payload: PartyUpdatePayload): Partial<Party> {
const mapped: any = {}
if (payload.name !== undefined) mapped.name = payload.name
if (payload.description !== undefined) mapped.description = payload.description
if (payload.element !== undefined) mapped.element = payload.element
if (payload.raidId !== undefined) mapped.raid = { id: payload.raidId }
if (payload.chargeAttack !== undefined) mapped.chargeAttack = payload.chargeAttack
if (payload.fullAuto !== undefined) mapped.fullAuto = payload.fullAuto
if (payload.autoGuard !== undefined) mapped.autoGuard = payload.autoGuard
if (payload.autoSummon !== undefined) mapped.autoSummon = payload.autoSummon
if (payload.clearTime !== undefined) mapped.clearTime = payload.clearTime
if (payload.buttonCount !== undefined) mapped.buttonCount = payload.buttonCount
if (payload.chainCount !== undefined) mapped.chainCount = payload.chainCount
if (payload.turnCount !== undefined) mapped.turnCount = payload.turnCount
if (payload.jobId !== undefined) mapped.job = { id: payload.jobId }
if (payload.visibility !== undefined) mapped.visibility = payload.visibility
if (payload.localId !== undefined) mapped.localId = payload.localId
return mapped
}
}