Initial
This commit is contained in:
657
src/App.vue
Normal file
657
src/App.vue
Normal file
@@ -0,0 +1,657 @@
|
||||
<template>
|
||||
<fluent-design-system-provider accent-base-color="#2457a6" :base-layer-luminance="fluentLuminance">
|
||||
<v-app :theme="activeTheme">
|
||||
<LoginView v-if="!token" :error="error" :form="loginForm" :loading="loading" @login="login" />
|
||||
|
||||
<div v-else class="app-shell">
|
||||
<v-navigation-drawer class="rail" width="254" permanent>
|
||||
<div class="brand-lockup">
|
||||
<span class="brand-mark">MA</span>
|
||||
<span class="brand-name">MixedAssets</span>
|
||||
</div>
|
||||
<v-divider />
|
||||
<v-list nav density="compact" class="pa-3">
|
||||
<v-list-item
|
||||
v-for="item in visibleNavItems"
|
||||
:key="item.key"
|
||||
:active="activeView === item.key"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.label"
|
||||
rounded="sm"
|
||||
@click="activeView = item.key"
|
||||
/>
|
||||
</v-list>
|
||||
<template #append>
|
||||
<div class="pa-4">
|
||||
<div class="text-body-2 font-weight-bold">{{ user?.name }}</div>
|
||||
<div class="text-caption quiet">{{ user?.role }}</div>
|
||||
<v-btn class="mt-3" block variant="tonal" prepend-icon="mdi-logout" @click="logout">Sign out</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<TopNav
|
||||
:preferences="profilePreferences"
|
||||
:saving="preferenceSaving"
|
||||
:subtitle="currentSubtitle"
|
||||
:theme-items="themeItems"
|
||||
:title="currentTitle"
|
||||
:user="user"
|
||||
@logout="logout"
|
||||
@save-preferences="saveProfilePreferences"
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn v-if="activeView === 'assets' && canEditAssets" color="primary" prepend-icon="mdi-plus" @click="newAsset">New asset</v-btn>
|
||||
<v-btn v-if="activeView === 'reports'" color="secondary" prepend-icon="mdi-file-pdf-box" @click="downloadPdf">PDF</v-btn>
|
||||
</template>
|
||||
</TopNav>
|
||||
|
||||
<v-main>
|
||||
<DashboardView
|
||||
v-if="activeView === 'dashboard'"
|
||||
:category-chart="categoryChart"
|
||||
:chart-options="chartOptions"
|
||||
:currency="currency"
|
||||
:dashboard="dashboard"
|
||||
:short-date="shortDate"
|
||||
/>
|
||||
<AssetsView
|
||||
v-if="activeView === 'assets'"
|
||||
:all-selected="allSelected"
|
||||
:assets="assets"
|
||||
:asset-filters="assetFilters"
|
||||
:currency="currency"
|
||||
:selected-asset-ids="selectedAssetIds"
|
||||
:status-items="statusItems"
|
||||
@edit-asset="editAsset"
|
||||
@export-assets="exportAssets"
|
||||
@filter-assets="filterAssets"
|
||||
@import-assets="importAssets"
|
||||
@open-mass-edit="massDialog = true"
|
||||
@toggle-all="toggleAll"
|
||||
@toggle-asset="toggleAsset"
|
||||
/>
|
||||
<AssignmentsView
|
||||
v-if="activeView === 'assignments'"
|
||||
:assignments="assignments"
|
||||
:assets="assets"
|
||||
:assignment-saving="assignmentSaving"
|
||||
:employees="employees"
|
||||
:short-date="shortDate"
|
||||
:summary="assignmentSummary"
|
||||
@add-employee="addEmployee"
|
||||
@assign-asset="assignAsset"
|
||||
@release-assignment="releaseAssignment"
|
||||
@reload="loadAssignments"
|
||||
/>
|
||||
<ReportsView
|
||||
v-if="activeView === 'reports'"
|
||||
:book-items="bookItems"
|
||||
:chart-options="chartOptions"
|
||||
:currency="currency"
|
||||
:depreciation-report="depreciationReport"
|
||||
:monthly-chart="monthlyChart"
|
||||
:report-filters="reportFilters"
|
||||
@filter-reports="filterReports"
|
||||
/>
|
||||
<TemplatesView
|
||||
v-if="activeView === 'templates'"
|
||||
:category-items="categoryItems"
|
||||
:custom-field-types="customFieldTypes"
|
||||
:method-items="methodItems"
|
||||
:template-form="templateForm"
|
||||
:templates="templates"
|
||||
@save-template="saveTemplate"
|
||||
/>
|
||||
<AdminView
|
||||
v-if="activeView === 'admin'"
|
||||
:role-capabilities="roleCapabilities"
|
||||
:role-permissions="rolePermissions"
|
||||
:roles="roles"
|
||||
:short-date="shortDate"
|
||||
:settings="settings"
|
||||
:tax-rules="taxRules"
|
||||
:team-saving="teamSaving"
|
||||
:teams="teams"
|
||||
:user-saving="userSaving"
|
||||
:users="users"
|
||||
:workday-connection="workdayConnection"
|
||||
:workday-saving="workdaySaving"
|
||||
:workday-syncing="workdaySyncing"
|
||||
@create-team="createTeam"
|
||||
@create-user="createUser"
|
||||
@save-settings="saveSettings"
|
||||
@save-workday="saveWorkdayConnection"
|
||||
@sync-workday="syncWorkday"
|
||||
@update-user="updateUser"
|
||||
/>
|
||||
</v-main>
|
||||
|
||||
<AssetDrawer
|
||||
v-model="assetDrawer"
|
||||
:asset-form="assetForm"
|
||||
:category-items="categoryItems"
|
||||
:drawer-error="drawerError"
|
||||
:entity-options="entityOptions"
|
||||
:method-items="methodItems"
|
||||
:saving="saving"
|
||||
:selected-template-fields="selectedTemplateFields"
|
||||
:selected-template-id="selectedTemplateId"
|
||||
:status-items="statusItems"
|
||||
:template-options="templateOptions"
|
||||
@save-asset="saveAsset"
|
||||
@select-template="applyTemplate"
|
||||
/>
|
||||
|
||||
<MassEditDialog
|
||||
v-model="massDialog"
|
||||
:changes="massChanges"
|
||||
:status-items="statusItems"
|
||||
@apply="massUpdate"
|
||||
/>
|
||||
</div>
|
||||
</v-app>
|
||||
</fluent-design-system-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminView from './views/AdminView.vue';
|
||||
import AssignmentsView from './views/AssignmentsView.vue';
|
||||
import AssetsView from './views/AssetsView.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
import LoginView from './views/LoginView.vue';
|
||||
import ReportsView from './views/ReportsView.vue';
|
||||
import TemplatesView from './views/TemplatesView.vue';
|
||||
import AssetDrawer from './components/AssetDrawer.vue';
|
||||
import MassEditDialog from './components/MassEditDialog.vue';
|
||||
import TopNav from './components/TopNav.vue';
|
||||
import { apiRequest, loginRequest, saveBlob } from './services/api';
|
||||
import { blankAsset, defaultValueForField, normalizeCustomFields } from './utils/assets';
|
||||
import { currency, shortDate } from './utils/format';
|
||||
import { bookItems, customFieldTypes, navItems, statusItems, themeItems } from './constants';
|
||||
|
||||
const emptyStats = {
|
||||
asset_count: 0,
|
||||
total_cost: 0,
|
||||
disposed_count: 0,
|
||||
in_service_count: 0
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AdminView,
|
||||
AssignmentsView,
|
||||
AssetDrawer,
|
||||
AssetsView,
|
||||
DashboardView,
|
||||
LoginView,
|
||||
MassEditDialog,
|
||||
ReportsView,
|
||||
TemplatesView,
|
||||
TopNav
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeView: 'dashboard',
|
||||
assets: [],
|
||||
assetDrawer: false,
|
||||
assetFilters: { search: '', status: null },
|
||||
assetForm: blankAsset(),
|
||||
assignments: [],
|
||||
assignmentSaving: false,
|
||||
assignmentSummary: {},
|
||||
bookItems,
|
||||
chartOptions: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true, ticks: { callback: (value) => `$${Number(value).toLocaleString()}` } } }
|
||||
},
|
||||
customFieldTypes,
|
||||
dashboard: { stats: { ...emptyStats }, byCategory: [], byStatus: [], upcomingTasks: [], auditTrail: [] },
|
||||
depreciationReport: { totals: { depreciation: 0 }, rows: [] },
|
||||
drawerError: '',
|
||||
employees: [],
|
||||
entities: [],
|
||||
error: '',
|
||||
loading: false,
|
||||
loginForm: { email: 'admin@mixedassets.local', password: 'ChangeMe123!' },
|
||||
massChanges: { status: null, location: '', department: '' },
|
||||
massDialog: false,
|
||||
monthlyReport: { months: [] },
|
||||
navItems,
|
||||
preferenceSaving: false,
|
||||
profilePreferences: { theme: localStorage.getItem('mixedassets.theme') || 'mixedAssetsDark' },
|
||||
references: [],
|
||||
reportFilters: { book: 'GAAP', year: new Date().getFullYear() },
|
||||
roleCapabilities: [],
|
||||
rolePermissions: {},
|
||||
roles: [],
|
||||
saving: false,
|
||||
selectedAssetIds: [],
|
||||
selectedTemplateId: null,
|
||||
settings: {},
|
||||
statusItems,
|
||||
taxRules: [],
|
||||
teamSaving: false,
|
||||
teams: [],
|
||||
templateForm: {
|
||||
name: '',
|
||||
description: '',
|
||||
defaults: { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'straight_line' },
|
||||
custom_fields: []
|
||||
},
|
||||
templates: [],
|
||||
themeItems,
|
||||
token: localStorage.getItem('mixedassets.token') || '',
|
||||
user: JSON.parse(localStorage.getItem('mixedassets.user') || 'null'),
|
||||
userSaving: false,
|
||||
users: [],
|
||||
workdayConnection: {},
|
||||
workdaySaving: false,
|
||||
workdaySyncing: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allSelected() {
|
||||
return this.assets.length > 0 && this.selectedAssetIds.length === this.assets.length;
|
||||
},
|
||||
categoryChart() {
|
||||
return {
|
||||
labels: this.dashboard.byCategory.map((row) => row.category),
|
||||
datasets: [{ data: this.dashboard.byCategory.map((row) => row.value), backgroundColor: '#2457a6' }]
|
||||
};
|
||||
},
|
||||
categoryItems() {
|
||||
const fromRefs = this.references.filter((item) => item.type === 'category').map((item) => item.name);
|
||||
return [...new Set([...fromRefs, 'Computer Equipment', 'Office Furniture', 'Vehicles', 'Leasehold Improvements', 'Uncategorized'])];
|
||||
},
|
||||
canEditAssets() {
|
||||
return ['admin', 'finance', 'operations'].includes(this.user?.role);
|
||||
},
|
||||
currentSubtitle() {
|
||||
return {
|
||||
dashboard: 'Portfolio value, lifecycle activity, and open work',
|
||||
assets: 'Asset register, imports, exports, and lifecycle updates',
|
||||
assignments: 'Assign assets to people, departments, and locations with full custody history',
|
||||
reports: 'Depreciation schedules, monthly expense, and book values',
|
||||
templates: 'JSON-driven entry defaults and custom fields',
|
||||
admin: 'Users, database, CORS, and tax-rule configuration'
|
||||
}[this.activeView];
|
||||
},
|
||||
currentTitle() {
|
||||
return {
|
||||
dashboard: 'Dashboard',
|
||||
assets: 'Asset register',
|
||||
assignments: 'Assignments',
|
||||
reports: 'Reports',
|
||||
templates: 'Templates',
|
||||
admin: 'Configuration'
|
||||
}[this.activeView];
|
||||
},
|
||||
entityOptions() {
|
||||
return this.entities.map((entity) => ({ title: entity.name, value: entity.id }));
|
||||
},
|
||||
activeTheme() {
|
||||
return this.profilePreferences.theme || 'mixedAssetsDark';
|
||||
},
|
||||
fluentLuminance() {
|
||||
return this.activeTheme === 'mixedAssetsDark' ? '0.12' : '1';
|
||||
},
|
||||
methodItems() {
|
||||
const methods = this.taxRules[0]?.rules_json?.methods || [];
|
||||
const fallback = [
|
||||
{ code: 'straight_line', family: 'GAAP', label: 'Straight-line' },
|
||||
{ code: 'sum_of_years_digits', family: 'GAAP', label: 'Sum-of-years-digits' },
|
||||
{ code: 'declining_balance_200', family: 'GAAP', label: '200% declining balance' },
|
||||
{ code: 'manual', family: 'Manual', label: 'Manual depreciation entry' }
|
||||
];
|
||||
return (methods.length ? methods : fallback).map((method) => ({
|
||||
title: `${method.family} · ${method.label}`,
|
||||
value: method.code
|
||||
}));
|
||||
},
|
||||
monthlyChart() {
|
||||
return {
|
||||
labels: this.monthlyReport.months.map((row) => `M${row.month}`),
|
||||
datasets: [{ data: this.monthlyReport.months.map((row) => row.depreciation), backgroundColor: '#006c6a' }]
|
||||
};
|
||||
},
|
||||
selectedTemplate() {
|
||||
return this.templates.find((template) => template.id === this.selectedTemplateId) || null;
|
||||
},
|
||||
selectedTemplateFields() {
|
||||
return normalizeCustomFields(this.selectedTemplate?.custom_fields || []);
|
||||
},
|
||||
templateOptions() {
|
||||
return this.templates.map((template) => ({ title: template.name, value: template.id }));
|
||||
},
|
||||
visibleNavItems() {
|
||||
const allowedByRole = {
|
||||
admin: ['dashboard', 'assets', 'assignments', 'reports', 'templates', 'admin'],
|
||||
finance: ['dashboard', 'assets', 'assignments', 'reports', 'templates'],
|
||||
operations: ['dashboard', 'assets', 'assignments'],
|
||||
viewer: ['dashboard', 'assets', 'assignments', 'reports', 'templates']
|
||||
};
|
||||
const allowed = allowedByRole[this.user?.role] || allowedByRole.viewer;
|
||||
return this.navItems.filter((item) => allowed.includes(item.key));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeView(view) {
|
||||
if (view === 'reports') this.loadReports();
|
||||
if (view === 'admin') this.loadAdmin();
|
||||
if (view === 'assignments') this.loadAssignments();
|
||||
},
|
||||
visibleNavItems(items) {
|
||||
if (!items.some((item) => item.key === this.activeView)) {
|
||||
this.activeView = items[0]?.key || 'dashboard';
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.token) this.loadInitial();
|
||||
},
|
||||
methods: {
|
||||
currency,
|
||||
shortDate,
|
||||
async api(path, options = {}) {
|
||||
return apiRequest(path, this.token, options);
|
||||
},
|
||||
async addEmployee(employee) {
|
||||
await this.api('/api/employees', { method: 'POST', body: JSON.stringify(employee) });
|
||||
await this.loadAssignments();
|
||||
},
|
||||
applyTemplate(id) {
|
||||
this.selectedTemplateId = id || null;
|
||||
const template = this.templates.find((item) => item.id === id);
|
||||
if (!template) {
|
||||
this.assetForm = { ...this.assetForm, template_id: null };
|
||||
return;
|
||||
}
|
||||
const customFields = { ...(this.assetForm.custom_fields || {}) };
|
||||
for (const field of normalizeCustomFields(template.custom_fields)) {
|
||||
if (customFields[field.key] === undefined) customFields[field.key] = defaultValueForField(field);
|
||||
}
|
||||
this.assetForm = { ...this.assetForm, ...template.defaults, custom_fields: customFields, template_id: template.id };
|
||||
},
|
||||
async assignAsset(form) {
|
||||
this.assignmentSaving = true;
|
||||
try {
|
||||
await this.api('/api/assignments', { method: 'POST', body: JSON.stringify(form) });
|
||||
await Promise.all([this.loadAssignments(), this.loadAssets(), this.loadDashboard()]);
|
||||
} finally {
|
||||
this.assignmentSaving = false;
|
||||
}
|
||||
},
|
||||
async createTeam(team) {
|
||||
this.teamSaving = true;
|
||||
try {
|
||||
await this.api('/api/teams', { method: 'POST', body: JSON.stringify(team) });
|
||||
await this.loadAdmin();
|
||||
} finally {
|
||||
this.teamSaving = false;
|
||||
}
|
||||
},
|
||||
async createUser(user) {
|
||||
this.userSaving = true;
|
||||
try {
|
||||
await this.api('/api/users', { method: 'POST', body: JSON.stringify(user) });
|
||||
await this.loadAdmin();
|
||||
} finally {
|
||||
this.userSaving = false;
|
||||
}
|
||||
},
|
||||
async downloadPdf() {
|
||||
const blob = await (await this.api(`/api/reports/depreciation.pdf?year=${this.reportFilters.year}&book=${this.reportFilters.book}`)).blob();
|
||||
saveBlob(blob, `mixedassets-${this.reportFilters.book}-${this.reportFilters.year}.pdf`);
|
||||
},
|
||||
editAsset(asset) {
|
||||
this.assetForm = { ...blankAsset(), ...asset, custom_fields: asset.custom_fields || {} };
|
||||
this.selectedTemplateId = asset.template_id || null;
|
||||
this.assetDrawer = true;
|
||||
},
|
||||
async exportAssets(format) {
|
||||
const blob = await (await this.api(`/api/export/assets?format=${format}`)).blob();
|
||||
saveBlob(blob, `mixedassets-assets.${format}`);
|
||||
},
|
||||
filterAssets(filters) {
|
||||
this.assetFilters = filters;
|
||||
this.loadAssets();
|
||||
},
|
||||
filterReports(filters) {
|
||||
this.reportFilters = filters;
|
||||
this.loadReports();
|
||||
},
|
||||
async importAssets(event) {
|
||||
const [file] = event.target.files || [];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await this.api('/api/import/assets', { method: 'POST', body: formData });
|
||||
event.target.value = '';
|
||||
await this.loadAssets();
|
||||
await this.loadDashboard();
|
||||
},
|
||||
async loadAdmin() {
|
||||
const [settings, rules, workday, users, teams, accessControl] = await Promise.all([
|
||||
this.api('/api/settings').then((r) => r.json()).catch(() => ({ settings: {} })),
|
||||
this.api('/api/tax-rules').then((r) => r.json()),
|
||||
this.api('/api/workday/connection').then((r) => r.json()).catch(() => ({ connection: {} })),
|
||||
this.api('/api/users').then((r) => r.json()).catch(() => ({ users: [] })),
|
||||
this.api('/api/teams').then((r) => r.json()).catch(() => ({ teams: [] })),
|
||||
this.api('/api/access-control').then((r) => r.json()).catch(() => ({
|
||||
roles: [],
|
||||
rolePermissions: {},
|
||||
roleCapabilities: []
|
||||
}))
|
||||
]);
|
||||
this.settings = settings.settings;
|
||||
this.taxRules = rules.ruleSets;
|
||||
this.workdayConnection = workday.connection || {};
|
||||
this.users = users.users || [];
|
||||
this.teams = teams.teams || [];
|
||||
this.roles = accessControl.roles || [];
|
||||
this.rolePermissions = accessControl.rolePermissions || {};
|
||||
this.roleCapabilities = accessControl.roleCapabilities || [];
|
||||
},
|
||||
async loadAssets() {
|
||||
const params = new URLSearchParams();
|
||||
if (this.assetFilters.search) params.set('search', this.assetFilters.search);
|
||||
if (this.assetFilters.status) params.set('status', this.assetFilters.status);
|
||||
const data = await (await this.api(`/api/assets?${params.toString()}`)).json();
|
||||
this.assets = data.assets;
|
||||
this.selectedAssetIds = this.selectedAssetIds.filter((id) => this.assets.some((asset) => asset.id === id));
|
||||
},
|
||||
async loadDashboard() {
|
||||
this.dashboard = await (await this.api('/api/dashboard')).json();
|
||||
},
|
||||
async loadInitial() {
|
||||
await Promise.all([
|
||||
this.loadProfile(),
|
||||
this.loadDashboard(),
|
||||
this.loadAssets(),
|
||||
this.loadAssignments(),
|
||||
this.api('/api/entities').then((r) => r.json()).then((data) => { this.entities = data.entities; }),
|
||||
this.api('/api/references').then((r) => r.json()).then((data) => { this.references = data.references; }),
|
||||
this.api('/api/templates').then((r) => r.json()).then((data) => { this.templates = data.templates; }),
|
||||
this.api('/api/tax-rules').then((r) => r.json()).then((data) => { this.taxRules = data.ruleSets; })
|
||||
]);
|
||||
},
|
||||
async loadAssignments() {
|
||||
const [employees, assignments, workday] = await Promise.all([
|
||||
this.api('/api/employees').then((r) => r.json()),
|
||||
this.api('/api/assignments').then((r) => r.json()),
|
||||
this.api('/api/workday/connection').then((r) => r.json()).catch(() => ({ connection: {} }))
|
||||
]);
|
||||
this.employees = employees.employees;
|
||||
this.assignments = assignments.assignments;
|
||||
this.assignmentSummary = assignments.summary;
|
||||
this.workdayConnection = workday.connection || {};
|
||||
},
|
||||
async loadProfile() {
|
||||
const profile = await (await this.api('/api/profile')).json();
|
||||
this.user = profile.user;
|
||||
this.profilePreferences = { theme: 'mixedAssetsDark', ...(profile.preferences || {}) };
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(profile.user));
|
||||
localStorage.setItem('mixedassets.theme', this.activeTheme);
|
||||
},
|
||||
async loadReports() {
|
||||
const query = `year=${this.reportFilters.year}&book=${this.reportFilters.book}`;
|
||||
const [annual, monthly] = await Promise.all([
|
||||
this.api(`/api/reports/depreciation?${query}`).then((r) => r.json()),
|
||||
this.api(`/api/reports/monthly-depreciation?${query}`).then((r) => r.json())
|
||||
]);
|
||||
this.depreciationReport = annual;
|
||||
this.monthlyReport = monthly;
|
||||
},
|
||||
async login(form) {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const data = await loginRequest(form);
|
||||
this.loginForm = { ...form };
|
||||
this.token = data.token;
|
||||
this.user = data.user;
|
||||
localStorage.setItem('mixedassets.token', data.token);
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(data.user));
|
||||
await this.loadInitial();
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.token = '';
|
||||
this.user = null;
|
||||
localStorage.removeItem('mixedassets.token');
|
||||
localStorage.removeItem('mixedassets.user');
|
||||
},
|
||||
async massUpdate(changesInput) {
|
||||
const changes = Object.fromEntries(Object.entries(changesInput).filter(([, value]) => value !== '' && value !== null));
|
||||
await this.api('/api/assets/mass-update', { method: 'POST', body: JSON.stringify({ ids: this.selectedAssetIds, changes }) });
|
||||
this.massDialog = false;
|
||||
this.massChanges = { status: null, location: '', department: '' };
|
||||
await this.loadAssets();
|
||||
await this.loadDashboard();
|
||||
},
|
||||
newAsset() {
|
||||
this.assetForm = blankAsset();
|
||||
this.assetForm.entity_id = this.entities[0]?.id || null;
|
||||
this.selectedTemplateId = null;
|
||||
this.drawerError = '';
|
||||
this.assetDrawer = true;
|
||||
},
|
||||
async saveAsset(assetInput) {
|
||||
this.saving = true;
|
||||
this.drawerError = '';
|
||||
try {
|
||||
const payload = {
|
||||
...assetInput,
|
||||
books: this.bookItems.map((book) => ({
|
||||
book_type: book,
|
||||
depreciation_method: assetInput.depreciation_method,
|
||||
useful_life_months: assetInput.useful_life_months,
|
||||
cost: assetInput.acquisition_cost
|
||||
}))
|
||||
};
|
||||
const method = assetInput.id ? 'PUT' : 'POST';
|
||||
const url = assetInput.id ? `/api/assets/${assetInput.id}` : '/api/assets';
|
||||
await this.api(url, { method, body: JSON.stringify(payload) });
|
||||
this.assetDrawer = false;
|
||||
await this.loadAssets();
|
||||
await this.loadDashboard();
|
||||
} catch (error) {
|
||||
this.drawerError = error.message;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async saveSettings(settings) {
|
||||
await this.api('/api/settings', { method: 'PUT', body: JSON.stringify({ settings }) });
|
||||
await this.loadAdmin();
|
||||
},
|
||||
async releaseAssignment(assignment) {
|
||||
await this.api(`/api/assignments/${assignment.id}/release`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ release_reason: 'Released from UI' })
|
||||
});
|
||||
await Promise.all([this.loadAssignments(), this.loadAssets()]);
|
||||
},
|
||||
async saveProfilePreferences(preferences, options = { persist: true }) {
|
||||
this.profilePreferences = { ...this.profilePreferences, ...(preferences || {}) };
|
||||
localStorage.setItem('mixedassets.theme', this.activeTheme);
|
||||
if (!options.persist) return;
|
||||
|
||||
this.preferenceSaving = true;
|
||||
try {
|
||||
const profile = await (await this.api('/api/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ preferences: this.profilePreferences })
|
||||
})).json();
|
||||
this.user = profile.user;
|
||||
this.profilePreferences = { theme: 'mixedAssetsDark', ...(profile.preferences || {}) };
|
||||
localStorage.setItem('mixedassets.user', JSON.stringify(profile.user));
|
||||
localStorage.setItem('mixedassets.theme', this.activeTheme);
|
||||
} finally {
|
||||
this.preferenceSaving = false;
|
||||
}
|
||||
},
|
||||
async saveWorkdayConnection(connection) {
|
||||
this.workdaySaving = true;
|
||||
try {
|
||||
const body = { ...connection };
|
||||
if (!body.client_secret) delete body.client_secret;
|
||||
await this.api('/api/workday/connection', { method: 'PUT', body: JSON.stringify(body) });
|
||||
await this.loadAssignments();
|
||||
} finally {
|
||||
this.workdaySaving = false;
|
||||
}
|
||||
},
|
||||
async saveTemplate(form) {
|
||||
const payload = {
|
||||
...form,
|
||||
custom_fields: normalizeCustomFields(form.custom_fields)
|
||||
};
|
||||
await this.api('/api/templates', { method: 'POST', body: JSON.stringify(payload) });
|
||||
const data = await (await this.api('/api/templates')).json();
|
||||
this.templates = data.templates;
|
||||
this.templateForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
defaults: { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'straight_line' },
|
||||
custom_fields: []
|
||||
};
|
||||
},
|
||||
async syncWorkday() {
|
||||
this.workdaySyncing = true;
|
||||
try {
|
||||
await this.api('/api/workday/sync-workers', { method: 'POST', body: JSON.stringify({}) });
|
||||
await this.loadAssignments();
|
||||
} finally {
|
||||
this.workdaySyncing = false;
|
||||
}
|
||||
},
|
||||
async updateUser(user) {
|
||||
this.userSaving = true;
|
||||
try {
|
||||
const body = { ...user };
|
||||
if (!body.password) delete body.password;
|
||||
await this.api(`/api/users/${user.id}`, { method: 'PUT', body: JSON.stringify(body) });
|
||||
await this.loadAdmin();
|
||||
} finally {
|
||||
this.userSaving = false;
|
||||
}
|
||||
},
|
||||
toggleAll(value) {
|
||||
this.selectedAssetIds = value ? this.assets.map((asset) => asset.id) : [];
|
||||
},
|
||||
toggleAsset(id) {
|
||||
this.selectedAssetIds = this.selectedAssetIds.includes(id)
|
||||
? this.selectedAssetIds.filter((assetId) => assetId !== id)
|
||||
: [...this.selectedAssetIds, id];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user