Add new services for business logic
This commit is contained in:
parent
cf351ef1fc
commit
32c1c9016e
3 changed files with 856 additions and 0 deletions
192
src/lib/services/conflict.service.ts
Normal file
192
src/lib/services/conflict.service.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import type { Party, GridWeapon, GridCharacter } from '$lib/api/schemas/party'
|
||||||
|
import type { FetchLike } from '$lib/api/core'
|
||||||
|
import * as partiesApi from '$lib/api/resources/parties'
|
||||||
|
|
||||||
|
export interface ConflictData {
|
||||||
|
conflicts: string[]
|
||||||
|
incoming: string
|
||||||
|
position: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConflictResolution {
|
||||||
|
action: 'replace' | 'cancel'
|
||||||
|
removeIds: string[]
|
||||||
|
addId: string
|
||||||
|
position: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conflict service - handles conflict resolution for weapons and characters
|
||||||
|
*/
|
||||||
|
export class ConflictService {
|
||||||
|
constructor(private fetch: FetchLike) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a conflict by choosing which items to keep
|
||||||
|
*/
|
||||||
|
async resolveConflict(
|
||||||
|
partyId: string,
|
||||||
|
conflictType: 'weapon' | 'character',
|
||||||
|
resolution: ConflictResolution,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const headers = this.buildHeaders(editKey)
|
||||||
|
|
||||||
|
if (conflictType === 'weapon') {
|
||||||
|
return this.resolveWeaponConflict(partyId, resolution, headers)
|
||||||
|
} else {
|
||||||
|
return this.resolveCharacterConflict(partyId, resolution, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if adding an item would cause conflicts
|
||||||
|
*/
|
||||||
|
checkConflicts(
|
||||||
|
party: Party,
|
||||||
|
itemType: 'weapon' | 'character',
|
||||||
|
itemId: string
|
||||||
|
): ConflictData | null {
|
||||||
|
if (itemType === 'weapon') {
|
||||||
|
return this.checkWeaponConflicts(party, itemId)
|
||||||
|
} else {
|
||||||
|
return this.checkCharacterConflicts(party, itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format conflict message for display
|
||||||
|
*/
|
||||||
|
formatConflictMessage(
|
||||||
|
conflictType: 'weapon' | 'character',
|
||||||
|
conflictingItems: Array<{ name: string; position: number }>,
|
||||||
|
incomingItem: { name: string }
|
||||||
|
): string {
|
||||||
|
const itemTypeLabel = conflictType === 'weapon' ? 'weapon' : 'character'
|
||||||
|
const conflictNames = conflictingItems.map(i => i.name).join(', ')
|
||||||
|
|
||||||
|
if (conflictingItems.length === 1) {
|
||||||
|
return `Adding ${incomingItem.name} would conflict with ${conflictNames}. Which ${itemTypeLabel} would you like to keep?`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Adding ${incomingItem.name} would conflict with: ${conflictNames}. Which ${itemTypeLabel}s would you like to keep?`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private methods
|
||||||
|
|
||||||
|
private async resolveWeaponConflict(
|
||||||
|
partyId: string,
|
||||||
|
resolution: ConflictResolution,
|
||||||
|
headers: Record<string, string>
|
||||||
|
): Promise<Party> {
|
||||||
|
// Build payload to remove conflicting weapons and add the new one
|
||||||
|
const payload = {
|
||||||
|
weapons: [
|
||||||
|
// Remove conflicting weapons
|
||||||
|
...resolution.removeIds.map(id => ({
|
||||||
|
id,
|
||||||
|
_destroy: true
|
||||||
|
})),
|
||||||
|
// Add the new weapon
|
||||||
|
{
|
||||||
|
weaponId: resolution.addId,
|
||||||
|
position: resolution.position,
|
||||||
|
uncapLevel: 0,
|
||||||
|
transcendenceLevel: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateWeaponGrid(this.fetch, partyId, payload, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveCharacterConflict(
|
||||||
|
partyId: string,
|
||||||
|
resolution: ConflictResolution,
|
||||||
|
headers: Record<string, string>
|
||||||
|
): Promise<Party> {
|
||||||
|
// Build payload to remove conflicting characters and add the new one
|
||||||
|
const payload = {
|
||||||
|
characters: [
|
||||||
|
// Remove conflicting characters
|
||||||
|
...resolution.removeIds.map(id => ({
|
||||||
|
id,
|
||||||
|
_destroy: true
|
||||||
|
})),
|
||||||
|
// Add the new character
|
||||||
|
{
|
||||||
|
characterId: resolution.addId,
|
||||||
|
position: resolution.position,
|
||||||
|
uncapLevel: 0,
|
||||||
|
transcendenceLevel: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateCharacterGrid(this.fetch, partyId, payload, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkWeaponConflicts(party: Party, weaponId: string): ConflictData | null {
|
||||||
|
// Check for duplicate weapons (simplified - actual logic would be more complex)
|
||||||
|
const existingWeapon = party.weapons.find(w => w.weapon.id === weaponId)
|
||||||
|
|
||||||
|
if (existingWeapon) {
|
||||||
|
return {
|
||||||
|
conflicts: [existingWeapon.id],
|
||||||
|
incoming: weaponId,
|
||||||
|
position: existingWeapon.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could check for other conflict types here (e.g., same series weapons)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkCharacterConflicts(party: Party, characterId: string): ConflictData | null {
|
||||||
|
// Check for duplicate characters
|
||||||
|
const existingCharacter = party.characters.find(c => c.character.id === characterId)
|
||||||
|
|
||||||
|
if (existingCharacter) {
|
||||||
|
return {
|
||||||
|
conflicts: [existingCharacter.id],
|
||||||
|
incoming: characterId,
|
||||||
|
position: existingCharacter.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for conflicts with other versions of the same character
|
||||||
|
// This would need character metadata to determine conflicts
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildHeaders(editKey?: string): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
if (editKey) {
|
||||||
|
headers['X-Edit-Key'] = editKey
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conflict constraints for a specific type
|
||||||
|
*/
|
||||||
|
getConflictConstraints(itemType: 'weapon' | 'character'): {
|
||||||
|
allowDuplicates: boolean
|
||||||
|
maxPerType?: number
|
||||||
|
checkVariants: boolean
|
||||||
|
} {
|
||||||
|
if (itemType === 'weapon') {
|
||||||
|
return {
|
||||||
|
allowDuplicates: false,
|
||||||
|
checkVariants: true // Check for same series weapons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowDuplicates: false,
|
||||||
|
checkVariants: true // Check for different versions of same character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
450
src/lib/services/grid.service.ts
Normal file
450
src/lib/services/grid.service.ts
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/api/schemas/party'
|
||||||
|
import * as partiesApi from '$lib/api/resources/parties'
|
||||||
|
import type { FetchLike } from '$lib/api/core'
|
||||||
|
|
||||||
|
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(private fetch: FetchLike) {}
|
||||||
|
|
||||||
|
// Weapon Grid Operations
|
||||||
|
|
||||||
|
async addWeapon(
|
||||||
|
partyId: string,
|
||||||
|
weaponId: string,
|
||||||
|
position: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<GridUpdateResult> {
|
||||||
|
const payload = {
|
||||||
|
weaponId,
|
||||||
|
position,
|
||||||
|
uncapLevel: 0,
|
||||||
|
transcendenceLevel: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
return { party }
|
||||||
|
} catch (error: any) {
|
||||||
|
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
|
||||||
|
): Promise<GridUpdateResult> {
|
||||||
|
const payload = {
|
||||||
|
id: gridWeaponId,
|
||||||
|
weaponId: newWeaponId
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
return { party }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.type === 'conflict') {
|
||||||
|
return {
|
||||||
|
party: null as any,
|
||||||
|
conflicts: error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeWeapon(
|
||||||
|
partyId: string,
|
||||||
|
gridWeaponId: string,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridWeaponId,
|
||||||
|
_destroy: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveWeapon(
|
||||||
|
partyId: string,
|
||||||
|
gridWeaponId: string,
|
||||||
|
newPosition: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridWeaponId,
|
||||||
|
position: newPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async swapWeapons(
|
||||||
|
partyId: string,
|
||||||
|
gridWeaponId1: string,
|
||||||
|
gridWeaponId2: string,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
swap: [gridWeaponId1, gridWeaponId2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWeaponUncap(
|
||||||
|
partyId: string,
|
||||||
|
gridWeaponId: string,
|
||||||
|
uncapLevel: number,
|
||||||
|
transcendenceLevel: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridWeaponId,
|
||||||
|
uncapLevel,
|
||||||
|
transcendenceLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uncap to 6 when transcending
|
||||||
|
if (transcendenceLevel > 0 && uncapLevel < 6) {
|
||||||
|
payload.uncapLevel = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateWeaponGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summon Grid Operations
|
||||||
|
|
||||||
|
async addSummon(
|
||||||
|
partyId: string,
|
||||||
|
summonId: string,
|
||||||
|
position: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
summonId,
|
||||||
|
position,
|
||||||
|
uncapLevel: 0,
|
||||||
|
transcendenceLevel: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateSummonGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async replaceSummon(
|
||||||
|
partyId: string,
|
||||||
|
gridSummonId: string,
|
||||||
|
newSummonId: string,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridSummonId,
|
||||||
|
summonId: newSummonId
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateSummonGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSummon(
|
||||||
|
partyId: string,
|
||||||
|
gridSummonId: string,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridSummonId,
|
||||||
|
_destroy: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateSummonGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSummonUncap(
|
||||||
|
partyId: string,
|
||||||
|
gridSummonId: string,
|
||||||
|
uncapLevel: number,
|
||||||
|
transcendenceLevel: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridSummonId,
|
||||||
|
uncapLevel,
|
||||||
|
transcendenceLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uncap to 6 when transcending
|
||||||
|
if (transcendenceLevel > 0 && uncapLevel < 6) {
|
||||||
|
payload.uncapLevel = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateSummonGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character Grid Operations
|
||||||
|
|
||||||
|
async addCharacter(
|
||||||
|
partyId: string,
|
||||||
|
characterId: string,
|
||||||
|
position: number,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<GridUpdateResult> {
|
||||||
|
const payload = {
|
||||||
|
characterId,
|
||||||
|
position,
|
||||||
|
uncapLevel: 0,
|
||||||
|
transcendenceLevel: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await partiesApi.updateCharacterGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
return { party }
|
||||||
|
} 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
|
||||||
|
): Promise<GridUpdateResult> {
|
||||||
|
const payload = {
|
||||||
|
id: gridCharacterId,
|
||||||
|
characterId: newCharacterId
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await partiesApi.updateCharacterGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
return { party }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.type === 'conflict') {
|
||||||
|
return {
|
||||||
|
party: null as any,
|
||||||
|
conflicts: error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCharacter(
|
||||||
|
partyId: string,
|
||||||
|
gridCharacterId: string,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridCharacterId,
|
||||||
|
_destroy: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateCharacterGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
this.buildHeaders(editKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCharacterUncap(
|
||||||
|
partyId: string,
|
||||||
|
gridCharacterId: string,
|
||||||
|
uncapLevel: number,
|
||||||
|
transcendenceLevel: number,
|
||||||
|
perpetuity: boolean,
|
||||||
|
editKey?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
const payload = {
|
||||||
|
id: gridCharacterId,
|
||||||
|
uncapLevel,
|
||||||
|
transcendenceLevel,
|
||||||
|
perpetuity
|
||||||
|
}
|
||||||
|
|
||||||
|
return partiesApi.updateCharacterGrid(
|
||||||
|
this.fetch,
|
||||||
|
partyId,
|
||||||
|
payload,
|
||||||
|
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.id === 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
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/lib/services/party.service.ts
Normal file
214
src/lib/services/party.service.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
import type { Party } from '$lib/api/schemas/party'
|
||||||
|
import * as partiesApi from '$lib/api/resources/parties'
|
||||||
|
import type { FetchLike } from '$lib/api/core'
|
||||||
|
|
||||||
|
export interface EditabilityResult {
|
||||||
|
canEdit: boolean
|
||||||
|
headers?: Record<string, string>
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartyUpdatePayload {
|
||||||
|
name?: string | null
|
||||||
|
description?: string | null
|
||||||
|
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(private fetch: FetchLike) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get party by shortcode
|
||||||
|
*/
|
||||||
|
async getByShortcode(shortcode: string): Promise<Party> {
|
||||||
|
return partiesApi.getByShortcode(this.fetch, shortcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new party
|
||||||
|
*/
|
||||||
|
async create(payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
|
||||||
|
const headers = this.buildHeaders(editKey)
|
||||||
|
const apiPayload = this.mapToApiPayload(payload)
|
||||||
|
return partiesApi.create(this.fetch, apiPayload, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update party details
|
||||||
|
*/
|
||||||
|
async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
|
||||||
|
const headers = this.buildHeaders(editKey)
|
||||||
|
const apiPayload = this.mapToApiPayload(payload)
|
||||||
|
return partiesApi.update(this.fetch, 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 partiesApi.update(this.fetch, 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 result = await partiesApi.remix(this.fetch, shortcode, localId, headers)
|
||||||
|
|
||||||
|
// Store edit key if returned
|
||||||
|
if (result.editKey && typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(`edit_key_${result.party.shortcode}`, result.editKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favorite a party
|
||||||
|
*/
|
||||||
|
async favorite(id: string): Promise<void> {
|
||||||
|
return partiesApi.favorite(this.fetch, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfavorite a party
|
||||||
|
*/
|
||||||
|
async unfavorite(id: string): Promise<void> {
|
||||||
|
return partiesApi.unfavorite(this.fetch, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a party
|
||||||
|
*/
|
||||||
|
async delete(id: string, editKey?: string): Promise<void> {
|
||||||
|
const headers = this.buildHeaders(editKey)
|
||||||
|
return partiesApi.deleteParty(this.fetch, id, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue