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 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 { 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 { 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 { 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 { return partyAdapter.favorite(id) } /** * Unfavorite a party */ async unfavorite(id: string): Promise { return partyAdapter.unfavorite(id) } /** * Delete a party */ async delete(id: string, editKey?: string): Promise { // 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 = {} 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 { const headers: Record = {} if (editKey) { headers['X-Edit-Key'] = editKey } return headers } private mapToApiPayload(payload: PartyUpdatePayload): Partial { 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 } }