Password Policy/App rename/PM Scheduling Improvements
This commit is contained in:
132
src/App.vue
132
src/App.vue
@@ -3,13 +3,17 @@
|
||||
<v-app :theme="activeTheme">
|
||||
<LoginView v-if="!token" :error="error" :form="loginForm" :loading="loading" @login="login" />
|
||||
|
||||
<div v-else-if="forcePasswordChange" class="forced-change-screen">
|
||||
<ChangePasswordCard :token="token" forced standalone @changed="completeForcedChange" @cancel="logout" />
|
||||
</div>
|
||||
|
||||
<div v-else class="app-shell">
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
<v-navigation-drawer class="rail" :width="254" :rail="navCollapsed" :rail-width="76" permanent aria-label="Primary navigation">
|
||||
<div class="brand-lockup">
|
||||
<template v-if="!navCollapsed">
|
||||
<span class="brand-mark">MA</span>
|
||||
<span class="brand-name">MixedAssets</span>
|
||||
<span class="brand-name">DepreCore</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-chevron-left" size="small" variant="text" title="Collapse menu" aria-label="Collapse navigation menu" @click="toggleNav" />
|
||||
</template>
|
||||
@@ -68,6 +72,7 @@
|
||||
:theme-items="themeItems"
|
||||
:title="currentTitle"
|
||||
:user="user"
|
||||
@change-password="changePasswordDialog = true"
|
||||
@logout="logout"
|
||||
@open-help="helpDialog = true"
|
||||
@save-preferences="saveProfilePreferences"
|
||||
@@ -148,7 +153,7 @@
|
||||
<TaxRulesView
|
||||
v-if="activeView === 'tax-rules'"
|
||||
:token="token"
|
||||
:dark="activeTheme === 'mixedAssetsDark'"
|
||||
:dark="activeTheme === 'depreCoreDark'"
|
||||
@updated="reloadTaxRules"
|
||||
/>
|
||||
<BooksView
|
||||
@@ -197,6 +202,7 @@
|
||||
@save-workday="saveWorkdayConnection"
|
||||
@sync-workday="syncWorkday"
|
||||
@update-user="updateUser"
|
||||
@unlock-user="unlockUser"
|
||||
/>
|
||||
</v-main>
|
||||
|
||||
@@ -237,6 +243,7 @@
|
||||
@attach-pm="attachPm"
|
||||
@detach-pm="detachPm"
|
||||
@complete-pm="completePm"
|
||||
@log-reading="logReading"
|
||||
@view-completion="viewCompletion"
|
||||
@add-adjustment="addAdjustment"
|
||||
@add-note="addNote"
|
||||
@@ -276,6 +283,14 @@
|
||||
<PmCompletionDialog v-model="completionDialog" :token="token" :completion-id="completionId" />
|
||||
|
||||
<UserManual v-model="helpDialog" />
|
||||
|
||||
<v-dialog v-model="changePasswordDialog" max-width="480">
|
||||
<ChangePasswordCard :token="token" @changed="changePasswordDialog = false" @cancel="changePasswordDialog = false" />
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="5000" location="top end">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</v-app>
|
||||
</fluent-design-system-provider>
|
||||
@@ -290,6 +305,7 @@ import ContactsView from './views/ContactsView.vue';
|
||||
import AssetsView from './views/AssetsView.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
import LoginView from './views/LoginView.vue';
|
||||
import ChangePasswordCard from './components/ChangePasswordCard.vue';
|
||||
import PartsView from './views/PartsView.vue';
|
||||
import PmView from './views/PmView.vue';
|
||||
import ReportsView from './views/ReportsView.vue';
|
||||
@@ -322,6 +338,7 @@ export default {
|
||||
AssetDrawer,
|
||||
AssetsView,
|
||||
BooksView,
|
||||
ChangePasswordCard,
|
||||
ContactsView,
|
||||
DashboardView,
|
||||
LabelSheet,
|
||||
@@ -378,26 +395,29 @@ export default {
|
||||
entities: [],
|
||||
error: '',
|
||||
helpDialog: false,
|
||||
changePasswordDialog: false,
|
||||
snackbar: { show: false, text: '', color: 'error' },
|
||||
labelDialog: false,
|
||||
labelAssetsOverride: null,
|
||||
loading: false,
|
||||
loginForm: { email: 'admin@mixedassets.local', password: 'ChangeMe123!' },
|
||||
forcePasswordChange: false,
|
||||
loginForm: { email: 'admin@deprecore.local', password: 'ChangeMe123!' },
|
||||
massChanges: { status: null, location: '', department: '' },
|
||||
massDialog: false,
|
||||
navCollapsed: localStorage.getItem('mixedassets.navCollapsed') === 'true',
|
||||
navCollapsed: localStorage.getItem('deprecore.navCollapsed') === 'true',
|
||||
navItems,
|
||||
preferenceSaving: false,
|
||||
profilePreferences: {
|
||||
theme: localStorage.getItem('mixedassets.theme') || 'mixedAssetsDark',
|
||||
fontScale: localStorage.getItem('mixedassets.fontScale') || 'normal',
|
||||
accent: localStorage.getItem('mixedassets.accent') || '',
|
||||
contrast: localStorage.getItem('mixedassets.contrast') === 'true'
|
||||
theme: localStorage.getItem('deprecore.theme') || 'depreCoreDark',
|
||||
fontScale: localStorage.getItem('deprecore.fontScale') || 'normal',
|
||||
accent: localStorage.getItem('deprecore.accent') || '',
|
||||
contrast: localStorage.getItem('deprecore.contrast') === 'true'
|
||||
},
|
||||
references: [],
|
||||
workLocations: [],
|
||||
vendorContacts: [],
|
||||
depreciationZones: [],
|
||||
userCapabilities: JSON.parse(localStorage.getItem('mixedassets.capabilities') || '[]'),
|
||||
userCapabilities: JSON.parse(localStorage.getItem('deprecore.capabilities') || '[]'),
|
||||
capabilityCatalog: [],
|
||||
roles: [],
|
||||
saving: false,
|
||||
@@ -418,8 +438,8 @@ export default {
|
||||
},
|
||||
templates: [],
|
||||
themeItems,
|
||||
token: localStorage.getItem('mixedassets.token') || '',
|
||||
user: JSON.parse(localStorage.getItem('mixedassets.user') || 'null'),
|
||||
token: localStorage.getItem('deprecore.token') || '',
|
||||
user: JSON.parse(localStorage.getItem('deprecore.user') || 'null'),
|
||||
userSaving: false,
|
||||
users: [],
|
||||
workdayConnection: {},
|
||||
@@ -503,6 +523,8 @@ export default {
|
||||
'tax-rules': 'Upload, edit, export, and activate depreciation rule sets',
|
||||
admin: 'Users, configuration, and integrations',
|
||||
'admin-users': 'User accounts, teams, and role permissions',
|
||||
'admin-security': 'Password rules, account lockout, and complexity policy',
|
||||
'admin-audit': 'Complete audit trail of all user activity, with export',
|
||||
'admin-config': 'Application settings, maintenance defaults, and tax rule sets',
|
||||
'admin-integrations': 'Email, webhooks, ServiceNow, and Workday connectors'
|
||||
}[this.activeView];
|
||||
@@ -523,6 +545,8 @@ export default {
|
||||
'tax-rules': 'Tax rules',
|
||||
admin: 'Configuration',
|
||||
'admin-users': 'Users & Teams',
|
||||
'admin-security': 'Password & Security',
|
||||
'admin-audit': 'Activity & Audit Trail',
|
||||
'admin-config': 'Application Configuration',
|
||||
'admin-integrations': 'Integrations'
|
||||
}[this.activeView];
|
||||
@@ -539,7 +563,7 @@ export default {
|
||||
return this.assets;
|
||||
},
|
||||
activeTheme() {
|
||||
return this.profilePreferences.theme || 'mixedAssetsDark';
|
||||
return this.profilePreferences.theme || 'depreCoreDark';
|
||||
},
|
||||
fluentLuminance() {
|
||||
return darkThemes.includes(this.activeTheme) ? '0.12' : '1';
|
||||
@@ -581,6 +605,8 @@ export default {
|
||||
books: ['finance.manage'],
|
||||
'tax-rules': ['finance.manage'],
|
||||
'admin-users': ['admin.users'],
|
||||
'admin-security': ['admin.security'],
|
||||
'admin-audit': ['admin.audit'],
|
||||
'admin-config': ['admin.settings', 'config.manage'],
|
||||
'admin-integrations': ['admin.integrations']
|
||||
};
|
||||
@@ -624,11 +650,11 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.applyAppearance();
|
||||
window.addEventListener('mixedassets:unauthorized', this.handleUnauthorized);
|
||||
window.addEventListener('deprecore:unauthorized', this.handleUnauthorized);
|
||||
if (this.token) this.loadInitial();
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('mixedassets:unauthorized', this.handleUnauthorized);
|
||||
window.removeEventListener('deprecore:unauthorized', this.handleUnauthorized);
|
||||
},
|
||||
methods: {
|
||||
currency,
|
||||
@@ -643,7 +669,7 @@ export default {
|
||||
},
|
||||
setCapabilities(capabilities) {
|
||||
this.userCapabilities = Array.isArray(capabilities) ? capabilities : [];
|
||||
localStorage.setItem('mixedassets.capabilities', JSON.stringify(this.userCapabilities));
|
||||
localStorage.setItem('deprecore.capabilities', JSON.stringify(this.userCapabilities));
|
||||
},
|
||||
// Apply accessibility/appearance preferences to the live DOM: base text size (scales
|
||||
// the whole UI via the root font-size), a custom accent colour, and a high-contrast boost.
|
||||
@@ -677,10 +703,10 @@ export default {
|
||||
return lum > 0.6 ? '0,0,0' : '255,255,255';
|
||||
},
|
||||
persistAppearanceLocal() {
|
||||
localStorage.setItem('mixedassets.theme', this.activeTheme);
|
||||
localStorage.setItem('mixedassets.fontScale', this.profilePreferences.fontScale || 'normal');
|
||||
localStorage.setItem('mixedassets.accent', this.profilePreferences.accent || '');
|
||||
localStorage.setItem('mixedassets.contrast', String(Boolean(this.profilePreferences.contrast)));
|
||||
localStorage.setItem('deprecore.theme', this.activeTheme);
|
||||
localStorage.setItem('deprecore.fontScale', this.profilePreferences.fontScale || 'normal');
|
||||
localStorage.setItem('deprecore.accent', this.profilePreferences.accent || '');
|
||||
localStorage.setItem('deprecore.contrast', String(Boolean(this.profilePreferences.contrast)));
|
||||
},
|
||||
async api(path, options = {}) {
|
||||
return apiRequest(path, this.token, options);
|
||||
@@ -747,15 +773,30 @@ export default {
|
||||
this.teamNotice = error.message;
|
||||
}
|
||||
},
|
||||
notify(text, color = 'error') {
|
||||
this.snackbar = { show: true, text, color };
|
||||
},
|
||||
async createUser(user) {
|
||||
this.userSaving = true;
|
||||
try {
|
||||
await this.api('/api/users', { method: 'POST', body: JSON.stringify(user) });
|
||||
await this.loadAdmin();
|
||||
this.notify('User created.', 'success');
|
||||
} catch (error) {
|
||||
this.notify(error.message);
|
||||
} finally {
|
||||
this.userSaving = false;
|
||||
}
|
||||
},
|
||||
async unlockUser(account) {
|
||||
try {
|
||||
await this.api(`/api/users/${account.id}/unlock`, { method: 'POST' });
|
||||
await this.loadAdmin();
|
||||
this.notify(`${account.name} unlocked.`, 'success');
|
||||
} catch (error) {
|
||||
this.notify(error.message);
|
||||
}
|
||||
},
|
||||
async editAsset(asset) {
|
||||
this.drawerError = '';
|
||||
this.disposalPreview = null;
|
||||
@@ -926,6 +967,19 @@ export default {
|
||||
await this.refreshActiveAsset(this.assetForm.id);
|
||||
await this.loadDashboard();
|
||||
},
|
||||
async logReading(payload) {
|
||||
const { apId, ...body } = payload;
|
||||
try {
|
||||
await this.api(`/api/assets/${this.assetForm.id}/pm/${apId}/readings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
await this.refreshActiveAsset(this.assetForm.id);
|
||||
this.notify('Reading logged.', 'success');
|
||||
} catch (error) {
|
||||
this.notify(error.message);
|
||||
}
|
||||
},
|
||||
viewCompletion(id) {
|
||||
this.completionId = id;
|
||||
this.completionDialog = true;
|
||||
@@ -955,7 +1009,7 @@ export default {
|
||||
},
|
||||
async exportAssets(format) {
|
||||
const blob = await (await this.api(`/api/export/assets?format=${format}`)).blob();
|
||||
saveBlob(blob, `mixedassets-assets.${format}`);
|
||||
saveBlob(blob, `deprecore-assets.${format}`);
|
||||
},
|
||||
async loadReferences() {
|
||||
const data = await (await this.api('/api/references')).json();
|
||||
@@ -1068,9 +1122,9 @@ export default {
|
||||
async loadProfile() {
|
||||
const profile = await (await this.api('/api/profile')).json();
|
||||
this.user = profile.user;
|
||||
this.profilePreferences = { theme: 'mixedAssetsDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
|
||||
this.profilePreferences = { theme: 'depreCoreDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
|
||||
this.setCapabilities(profile.capabilities);
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(profile.user));
|
||||
localStorage.setItem('deprecore.user', JSON.stringify(profile.user));
|
||||
this.persistAppearanceLocal();
|
||||
},
|
||||
async login(form) {
|
||||
@@ -1082,22 +1136,35 @@ export default {
|
||||
this.token = data.token;
|
||||
this.user = data.user;
|
||||
this.setCapabilities(data.capabilities);
|
||||
localStorage.setItem('mixedassets.token', data.token);
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(data.user));
|
||||
await this.loadInitial();
|
||||
localStorage.setItem('deprecore.token', data.token);
|
||||
localStorage.setItem('deprecore.user', JSON.stringify(data.user));
|
||||
// A forced/expired password gates the app behind the change-password step (token is held
|
||||
// so the change endpoint is callable, but app data isn't loaded until the change succeeds).
|
||||
if (data.mustChangePassword) {
|
||||
this.forcePasswordChange = true;
|
||||
} else {
|
||||
await this.loadInitial();
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async completeForcedChange() {
|
||||
this.forcePasswordChange = false;
|
||||
this.error = '';
|
||||
this.loginForm = { ...this.loginForm, password: '' };
|
||||
await this.loadInitial();
|
||||
},
|
||||
logout() {
|
||||
this.token = '';
|
||||
this.user = null;
|
||||
this.forcePasswordChange = false;
|
||||
this.userCapabilities = [];
|
||||
localStorage.removeItem('mixedassets.token');
|
||||
localStorage.removeItem('mixedassets.user');
|
||||
localStorage.removeItem('mixedassets.capabilities');
|
||||
localStorage.removeItem('deprecore.token');
|
||||
localStorage.removeItem('deprecore.user');
|
||||
localStorage.removeItem('deprecore.capabilities');
|
||||
},
|
||||
// Triggered when any API call reports an invalid/expired token: return to the login screen.
|
||||
handleUnauthorized() {
|
||||
@@ -1115,7 +1182,7 @@ export default {
|
||||
},
|
||||
toggleNav() {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
localStorage.setItem('mixedassets.navCollapsed', String(this.navCollapsed));
|
||||
localStorage.setItem('deprecore.navCollapsed', String(this.navCollapsed));
|
||||
},
|
||||
// Group-only items (no own screen) just expand; real screens navigate.
|
||||
selectNav(item) {
|
||||
@@ -1204,8 +1271,8 @@ export default {
|
||||
body: JSON.stringify({ preferences: this.profilePreferences })
|
||||
})).json();
|
||||
this.user = profile.user;
|
||||
this.profilePreferences = { theme: 'mixedAssetsDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(profile.user));
|
||||
this.profilePreferences = { theme: 'depreCoreDark', fontScale: 'normal', accent: '', contrast: false, ...(profile.preferences || {}) };
|
||||
localStorage.setItem('deprecore.user', JSON.stringify(profile.user));
|
||||
this.persistAppearanceLocal();
|
||||
} finally {
|
||||
this.preferenceSaving = false;
|
||||
@@ -1267,6 +1334,9 @@ export default {
|
||||
if (!body.password) delete body.password;
|
||||
await this.api(`/api/users/${user.id}`, { method: 'PUT', body: JSON.stringify(body) });
|
||||
await this.loadAdmin();
|
||||
this.notify('User updated.', 'success');
|
||||
} catch (error) {
|
||||
this.notify(error.message);
|
||||
} finally {
|
||||
this.userSaving = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user