Files
habitica/website/client/src/components/admin-panel/user-support/itemsOwned.vue
Phillip Thelen 379afa9554 Improve Adminpanel with local logs (#15404)
* log armoire, quoest response and cron events to history

* show user history in admin panel

* allow stats to be edited from admin panel

* Improve admin panel stats input

* improve setting client in history

* fix tests

* fix lint

* fix armoire buying issue

* Improve hero saving

* Formatting fix

* Improve user history logging

* allow class to be changed from admin panel

* make terminating subscriptions easier

* support decimal extraMonths

* Fix editing some achievements in admin panel

* log if a user invites party to quest

* Log more quest events into user history

* make userhistory length configurable

* fix some numbered achievements

* fix extraMonths field

* Automatically set up group plan subs with admin panel

* show party info nicer in admin panel

* improve admin panel sub handling

* add missing brace

* display when there are unsaved changes

* fix setting group plan

* fix showing group id

* Display group plan info in admin panel

* fix setting hourglass promo date

* Improve termination handling in admin panel

* reload data after certain save events in admin panel

* remove console

* fix plan.extraMonths not being reset if terminating a sub

* add more options when cancelling subs

* reload data after group plan change

* Add a way to remove users from a party

* fix issue with removing user from party

* pass party id correctly

* correctly call async function

* Improve sub display in admin panel

* fix line length

* fix line

* shorter

* plaid

* fix(lint): vue code style

---------

Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2025-03-17 16:48:21 -05:00

307 lines
9.1 KiB
Vue

<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Items
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div>
The sections below display each item's key (bolded if the player has ever owned it),
followed by the item's English name.
<ul>
<li>
Click on an item's key or value to change it
(hovering shows an underline to show where you can click).
</li>
<li>For Mounts and Gear, clicking toggles between the allowed values.</li>
<li>For other item types, clicking gives you a form field to enter a new value.</li>
<li>Click Save when the correct value is displayed.</li>
<li>
You must Save for each item individually but you do not need to reload the user
between each Save.
</li>
<li>If you adjust an item and do not click Save for it, the change will be lost.</li>
</ul>
</div>
<div
v-for="itemType in itemTypes"
:key="itemType"
>
<div class="accordion-group">
<h4
class="expand-toggle"
:class="{'open': expandItemType[itemType]}"
@click="expandItemType[itemType] = !expandItemType[itemType]"
>
{{ itemType }}
</h4>
<div v-if="expandItemType[itemType]">
<p v-if="itemType === 'pets'">
A value of -1 means they owned the Pet but Released it
and have not yet rehatched it.
</p>
<p v-if="itemType === 'mounts'">
A value of "null" means they owned the Mount but Released it
and have not yet retamed it.
</p>
<p v-if="itemType === 'special'">
When there are 0 of these items, we can't tell if
they had been owned and were all used, or have never been owned.
</p>
<p v-if="itemType === 'gear'">
A value of true means they own the item now and can wear it.
A value of false means they used to own it but lost it from Death
(or an old Rebirth).
</p>
<ul>
<li
v-for="item in collatedItemData[itemType]"
:key="item.path"
>
<form @submit.prevent="saveItem(item)">
<span
class="enableValueChange"
@click="enableValueChange(item)"
>
<span :class="item.value ? 'owned' : 'not-owned'">
{{ item | displayValue }}
</span>
:
<span :class="{ ownedItem: !item.neverOwned }">{{ item.key }} : </span>
</span>
<span v-html="item.name"></span>
<div
v-if="item.modified"
class="form-inline"
>
<input
v-if="item.valueIsInteger"
v-model="item.value"
class="form-control valueField"
type="number"
>
<input
v-if="item.modified"
type="submit"
value="Save"
class="btn btn-primary"
>
</div>
</form>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
ul li {
margin-bottom: 0.2em;
}
.ownedItem {
font-weight: bold;
}
.enableValueChange:hover {
text-decoration: underline;
cursor: pointer;
}
.valueField {
min-width: 10ch;
}
.owned {
color: green;
}
.not-owned {
color: red;
}
</style>
<script>
import content from '@/../../common/script/content';
import getItemDescription from '../mixins/getItemDescription';
import saveHero from '../mixins/saveHero';
function collateItemData (self) {
const collatedItemData = {};
self.itemTypes.forEach(itemType => {
// itemTypes are pets, food, gear, etc
// Set up some basic data for this itemType:
let basePath = `items.${itemType}`;
let ownedItems = self.hero.items[itemType] || {};
let allItems = content[itemType];
if (itemType === 'gear') {
basePath = 'items.gear.owned';
ownedItems = self.hero.items.gear.owned || {};
allItems = content.gear.flat;
} else if (itemType === 'pets' || itemType === 'mounts') {
// add the non-Standard pets and mounts
const ucItemType = (itemType === 'pets') ? 'Pets' : 'Mounts';
self.petMountSubTypes.forEach(subType => {
allItems = { ...allItems, ...content[subType + ucItemType] };
});
}
const itemData = []; // all items for this itemType
// Collate data for items that the user owns or used to own:
for (const key of Object.keys(ownedItems)) {
// Do not sort keys. The order in the items object gives hints about order received.
if (itemType !== 'special' || self.specialItems.includes(key)) {
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
itemData.push({
neverOwned: false,
itemType,
key,
modified: false,
name: self.getItemDescription(itemType, key),
path: `${basePath}.${key}`,
value: ownedItems[key],
valueIsInteger,
});
}
}
// Collate data for items that the user never owned:
for (const key of Object.keys(allItems).sort()) {
if (
// ignore items the user owns because we captured them above:
!(key in ownedItems)
// ignore gear items that indicate empty equipped slots (e.g., head_base_0):
&& !(itemType === 'gear' && content.gear.flat[key].set
&& content.gear.flat[key].set === 'base-0')
// ignore "special" items that aren't Snowballs, Seafoam, etc:
&& (itemType !== 'special' || self.specialItems.includes(key))
) {
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
const value = (valueIsInteger) ? 0 : '';
itemData.push({
neverOwned: true,
itemType,
key,
modified: false,
name: self.getItemDescription(itemType, key),
path: `${basePath}.${key}`,
value,
valueIsInteger,
});
}
}
collatedItemData[itemType] = itemData;
});
return collatedItemData;
}
function resetData (self) {
self.collatedItemData = collateItemData(self);
self.itemTypes.forEach(itemType => { self.expandItemType[itemType] = false; });
}
export default {
filters: {
displayValue (item) {
if (item.value === '') return 'never owned';
if (item.value === 0 && item.neverOwned) return '0 (never owned)';
if (item.value === null) return 'null'; // we need visible text
return item.value; // true or false or an integer
},
},
mixins: [
getItemDescription,
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
expandItemType: {
eggs: false,
hatchingPotions: false,
food: false,
pets: false,
mounts: false,
quests: false,
gear: false,
special: false,
},
itemTypes: ['eggs', 'hatchingPotions', 'food', 'pets', 'mounts', 'quests', 'gear', 'special'],
nonIntegerTypes: ['mounts', 'gear'],
petMountSubTypes: ['premium', 'quest', 'special', 'wacky'], // e.g., 'premiumPets'
// items.special includes many things but we are interested in these only:
specialItems: ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'],
collatedItemData: {},
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
const toSave = {
_id: this.hero._id,
};
toSave.itemPath = item.path;
if (item.value === null) {
toSave.itemVal = 'null';
} else if (item.value === false) {
toSave.itemVal = 'false';
} else {
toSave.itemVal = item.value;
}
await this.saveHero({ hero: toSave, msg: item.key });
item.neverOwned = false;
item.modified = false;
},
enableValueChange (item) {
// allow form field(s) to be shown:
item.modified = true;
// for non-integer items, toggle through the allowed values:
if (item.itemType === 'gear' || item.itemType === 'mounts') {
// Allowed starting values are true, false, and undefined (never owned)
if (item.value && item.value !== '') {
item.value = false;
} else if (typeof item.value === 'boolean') {
item.value = '';
} else {
item.value = true;
}
}
},
},
};
</script>