Files
habitica/website/client/src/components/admin-panel/user-support/index.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

277 lines
7.5 KiB
Vue

<template>
<div v-if="hasPermission(user, 'userSupport')">
<div
v-if="hero && hero.profile"
class="row"
>
<div class="form col-12">
<basic-details
:user-id="hero._id"
:auth="hero.auth"
:preferences="hero.preferences"
:profile="hero.profile"
/>
<privileges-and-gems
:hero="hero"
:reset-counter="resetCounter"
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
[hero.auth, unModifiedHero.auth],
[hero.balance, unModifiedHero.balance],
[hero.secret, unModifiedHero.secret])"
/>
<subscription-and-perks
:hero="hero"
:group-plans="groupPlans"
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
unModifiedHero.purchased.plan])"
/>
<cron-and-auth
:hero="hero"
:reset-counter="resetCounter"
/>
<user-profile
:hero="hero"
:reset-counter="resetCounter"
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
/>
<party-and-quest
v-if="adminHasPrivForParty"
:user-id="hero._id"
:username="hero.auth.local.username"
:user-has-party="hasParty"
:party-not-exist-error="partyNotExistError"
:user-party-data="hero.party"
:group-party-data="party"
:reset-counter="resetCounter"
/>
<avatar-and-drops
:items="hero.items"
:preferences="hero.preferences"
/>
<stats
:hero="hero"
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
:reset-counter="resetCounter"
/>
<items-owned
:hero="hero"
:reset-counter="resetCounter"
/>
<customizations-owned
:hero="hero"
:reset-counter="resetCounter"
/>
<achievements
:hero="hero"
:reset-counter="resetCounter"
/>
<transactions
:hero="hero"
:reset-counter="resetCounter"
/>
<user-history
:hero="hero"
:reset-counter="resetCounter"
/>
<contributor-details
:hero="hero"
:hasUnsavedChanges="hasUnsavedChanges(
[hero.contributor, unModifiedHero.contributor],
[hero.permissions, unModifiedHero.permissions],
[hero.secret, unModifiedHero.secret],
)"
:reset-counter="resetCounter"
@clear-data="clearData"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep .accordion-group .accordion-group {
margin-left: 1em;
}
::v-deep h3 {
margin-top: 2em;
}
::v-deep h4 {
margin-top: 1em;
}
::v-deep .expand-toggle::after {
margin-left: 5px;
}
::v-deep .subsection-start {
margin-top: 1em;
}
::v-deep .form-inline {
margin-bottom: 1em;
input, span {
margin-left: 5px;
}
}
::v-deep .errorMessage {
font-weight: bold;
}
::v-deep .markdownPreview {
margin-left: 3em;
margin-top: 1em;
}
</style>
<script>
import isEqualWith from 'lodash/isEqualWith';
import BasicDetails from './basicDetails';
import ItemsOwned from './itemsOwned';
import CronAndAuth from './cronAndAuth';
import UserProfile from './userProfile';
import PartyAndQuest from './partyAndQuest';
import AvatarAndDrops from './avatarAndDrops';
import PrivilegesAndGems from './privilegesAndGems';
import ContributorDetails from './contributorDetails';
import Transactions from './transactions';
import SubscriptionAndPerks from './subscriptionAndPerks';
import CustomizationsOwned from './customizationsOwned.vue';
import Achievements from './achievements.vue';
import UserHistory from './userHistory.vue';
import Stats from './stats.vue';
import { userStateMixin } from '../../../mixins/userState';
export default {
components: {
BasicDetails,
ItemsOwned,
CustomizationsOwned,
CronAndAuth,
PartyAndQuest,
AvatarAndDrops,
PrivilegesAndGems,
ContributorDetails,
Transactions,
UserHistory,
Stats,
SubscriptionAndPerks,
UserProfile,
Achievements,
},
mixins: [userStateMixin],
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
data () {
return {
userIdentifier: '',
resetCounter: 0,
unModifiedHero: {},
hero: {},
party: {},
groupPlans: [],
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
};
},
watch: {
userIdentifier () {
// close modal if the page is opened in an existing tab from the modal
this.$root.$emit('bv::hide::modal', 'profile');
this.loadHero(this.userIdentifier);
},
},
mounted () {
this.userIdentifier = this.$route.params.userIdentifier;
},
methods: {
clearData () {
this.unModifiedHero = {};
this.hero = {};
},
async loadHero (userIdentifier) {
const id = userIdentifier.replace(/@/, ''); // allow "@name" to be entered
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
if (!this.hero.flags) {
this.hero.flags = {
chatRevoked: false,
chatShadowMuted: false,
};
}
if (!this.hero.permissions) {
this.hero.permissions = {};
}
this.hasParty = false;
this.partyNotExistError = false;
this.adminHasPrivForParty = true;
if (this.hero.party && this.hero.party._id) {
try {
this.party = await this.$store.dispatch('hall:getHeroParty', { groupId: this.hero.party._id });
this.hasParty = true;
} catch (e) {
if (e.message.includes('status code 401')) {
// @TODO is there a better way to recognise NotAuthorized error?
this.adminHasPrivForParty = false;
} else {
// the API's error message isn't worth reporting ("Request failed with status code 404")
this.partyNotExistError = true;
}
}
}
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
try {
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
} catch (e) {
this.groupPlans = [];
}
}
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
hasUnsavedChanges (...comparisons) {
for (const index in comparisons) {
if (index && comparisons[index]) {
const objs = comparisons[index];
const obj1 = objs[0];
const obj2 = objs[1];
if (!isEqualWith(obj1, obj2, (x, y) => {
if (typeof x === 'object' && typeof y === 'object') {
return undefined;
}
if (x === false && y === undefined) {
// Special case for checkboxes
return true;
}
return x == y; // eslint-disable-line eqeqeq
})) {
return true;
}
}
}
return false;
},
},
};
</script>