diff --git a/frontend/src/components/layout/DashboardLayout.vue b/frontend/src/components/layout/DashboardLayout.vue index 1ac701d..6be1976 100644 --- a/frontend/src/components/layout/DashboardLayout.vue +++ b/frontend/src/components/layout/DashboardLayout.vue @@ -27,6 +27,7 @@ import { AlertTriangle, FileText, FolderOpen, + Crosshair, Menu, X, } from 'lucide-vue-next' @@ -44,6 +45,7 @@ const navItems = [ { name: 'Players', path: '/players', icon: Users, permission: 'players.view' }, { name: 'Plugins', path: '/plugins', icon: Puzzle, permission: 'plugins.view' }, { name: 'File Manager', path: '/files', icon: FolderOpen, permission: 'files.view' }, + { name: 'Loot Builder', path: '/loot-builder', icon: Crosshair, permission: 'loot.view' }, { name: 'Auto-Wiper', path: '/wipes', icon: RefreshCw, permission: 'wipes.view' }, { name: 'Maps', path: '/maps', icon: Map, permission: 'maps.view' }, { name: 'Chat Log', path: '/chat', icon: MessageSquare, permission: 'chat.view' }, diff --git a/frontend/src/components/loot/LootContainerSidebar.vue b/frontend/src/components/loot/LootContainerSidebar.vue new file mode 100644 index 0000000..3a81c61 --- /dev/null +++ b/frontend/src/components/loot/LootContainerSidebar.vue @@ -0,0 +1,103 @@ + + + diff --git a/frontend/src/components/loot/LootGroupEditor.vue b/frontend/src/components/loot/LootGroupEditor.vue new file mode 100644 index 0000000..09a6551 --- /dev/null +++ b/frontend/src/components/loot/LootGroupEditor.vue @@ -0,0 +1,184 @@ + + + diff --git a/frontend/src/components/loot/LootItemEditor.vue b/frontend/src/components/loot/LootItemEditor.vue new file mode 100644 index 0000000..2776b21 --- /dev/null +++ b/frontend/src/components/loot/LootItemEditor.vue @@ -0,0 +1,232 @@ + + + diff --git a/frontend/src/components/loot/LootItemPicker.vue b/frontend/src/components/loot/LootItemPicker.vue new file mode 100644 index 0000000..9dac373 --- /dev/null +++ b/frontend/src/components/loot/LootItemPicker.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index f94554a..17c1dfd 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -110,6 +110,11 @@ const panelRoutes: RouteRecordRaw[] = [ name: 'files', component: () => import('@/views/admin/FileManagerView.vue'), }, + { + path: 'loot-builder', + name: 'loot-builder', + component: () => import('@/views/admin/LootBuilderView.vue'), + }, { path: 'wipes', name: 'wipes', diff --git a/frontend/src/stores/loot.ts b/frontend/src/stores/loot.ts new file mode 100644 index 0000000..405000f --- /dev/null +++ b/frontend/src/stores/loot.ts @@ -0,0 +1,179 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '@/composables/useApi' +import { useToastStore } from '@/stores/toast' +import type { LootProfileSummary, LootProfileFull, LootApplyResult } from '@/types' + +export const useLootStore = defineStore('loot', () => { + const profiles = ref([]) + const currentProfile = ref(null) + const selectedContainer = ref(null) + const isLoading = ref(false) + const isSaving = ref(false) + const isApplying = ref(false) + const isDirty = ref(false) + const api = useApi() + const toast = useToastStore() + + const activeProfile = computed(() => profiles.value.find(p => p.is_active) || null) + + async function fetchProfiles() { + isLoading.value = true + try { + const res = await api.get<{ profiles: LootProfileSummary[] }>('/loot/profiles') + profiles.value = res.profiles + } catch (err) { + toast.error((err as Error).message) + } finally { + isLoading.value = false + } + } + + async function loadProfile(id: string) { + isLoading.value = true + try { + const res = await api.get<{ profile: LootProfileFull }>(`/loot/profiles/${id}`) + currentProfile.value = res.profile + isDirty.value = false + // Select first container if none selected + if (!selectedContainer.value && currentProfile.value.loot_table) { + const keys = Object.keys(currentProfile.value.loot_table) + if (keys.length > 0) selectedContainer.value = keys[0] + } + } catch (err) { + toast.error((err as Error).message) + } finally { + isLoading.value = false + } + } + + async function createProfile(name: string, description?: string) { + try { + const res = await api.post<{ profile: LootProfileFull }>('/loot/profiles', { + profile_name: name, + description, + }) + await fetchProfiles() + currentProfile.value = res.profile + isDirty.value = false + toast.success(`Profile "${name}" created`) + return res.profile + } catch (err) { + toast.error((err as Error).message) + return null + } + } + + async function saveCurrentProfile() { + if (!currentProfile.value) return + isSaving.value = true + try { + await api.put(`/loot/profiles/${currentProfile.value.id}`, { + profile_name: currentProfile.value.profile_name, + description: currentProfile.value.description, + loot_table: currentProfile.value.loot_table, + loot_groups: currentProfile.value.loot_groups, + }) + isDirty.value = false + await fetchProfiles() + toast.success('Profile saved') + } catch (err) { + toast.error((err as Error).message) + } finally { + isSaving.value = false + } + } + + async function deleteProfile(id: string) { + try { + await api.del(`/loot/profiles/${id}`) + if (currentProfile.value?.id === id) { + currentProfile.value = null + selectedContainer.value = null + } + await fetchProfiles() + toast.success('Profile deleted') + } catch (err) { + toast.error((err as Error).message) + } + } + + async function duplicateProfile(id: string) { + try { + const res = await api.post<{ profile: LootProfileFull }>(`/loot/profiles/${id}/duplicate`) + await fetchProfiles() + toast.success(`Profile duplicated as "${res.profile.profile_name}"`) + return res.profile + } catch (err) { + toast.error((err as Error).message) + return null + } + } + + async function applyToServer(id: string, multiplier: number) { + isApplying.value = true + try { + const res = await api.post(`/loot/profiles/${id}/apply`, { multiplier }) + await fetchProfiles() + toast.success(res.message) + return res + } catch (err) { + toast.error((err as Error).message) + return null + } finally { + isApplying.value = false + } + } + + async function importProfile(name: string, lootTable: Record, lootGroups?: Record) { + try { + const res = await api.post<{ profile: LootProfileFull }>('/loot/import', { + profile_name: name, + loot_table: lootTable, + loot_groups: lootGroups || {}, + }) + await fetchProfiles() + toast.success(`Profile "${name}" imported`) + return res.profile + } catch (err) { + toast.error((err as Error).message) + return null + } + } + + async function exportProfile(id: string, multiplier: number) { + try { + return await api.get<{ profile_name: string; multiplier: number; loot_table: any; loot_groups: any }>( + `/loot/export/${id}?multiplier=${multiplier}`, + ) + } catch (err) { + toast.error((err as Error).message) + return null + } + } + + function markDirty() { + isDirty.value = true + } + + return { + profiles, + currentProfile, + selectedContainer, + isLoading, + isSaving, + isApplying, + isDirty, + activeProfile, + fetchProfiles, + loadProfile, + createProfile, + saveCurrentProfile, + deleteProfile, + duplicateProfile, + applyToServer, + importProfile, + exportProfile, + markDirty, + } +}) diff --git a/frontend/src/views/admin/LootBuilderView.vue b/frontend/src/views/admin/LootBuilderView.vue new file mode 100644 index 0000000..4f343dc --- /dev/null +++ b/frontend/src/views/admin/LootBuilderView.vue @@ -0,0 +1,393 @@ + + +