Settings Migration Guide - Frontend Developer
Overview
This guide will help you migrate settings from localStorage to the backend API. All account-level settings are now stored on the server and can be accessed via REST API endpoints.
API Endpoints
Base URL
- Production:
https://api.maintor.systems - Development: Your local API URL
Endpoints
1. Get Account Settings
GET /v1/accounts/{accountId}/settings
Authorization: IDTOKEN.<firebaseIdToken>
Response (200 OK):
{
"id": "settings_123",
"accountId": "account_456",
"ticketStatuses": {
"OPEN": true,
"IN_PROGRESS": true,
"COMPLETED": true,
"CLOSED": true,
"SKIPPED": true
},
"formSettings": {
"breakdownFormDesktop": {
"title": true,
"priority": true,
"status": true,
"description": true,
"breakdownEnded": true,
"laborEntries": true,
"rootCause": true,
"problemDescription": true,
"solutionDescription": true,
"downtime": true,
"photos": true,
"assignees": true,
"assetOwner": true,
"reportedBy": true,
"operatorNotes": true,
"visualId": true,
"barcode": true,
"qrCode": true
},
"breakdownFormMobile": {
"title": true,
"priority": true,
"status": true,
"description": true,
"breakdownEnded": true,
"laborEntries": true,
"rootCause": true,
"problemDescription": true,
"solutionDescription": true,
"downtime": true,
"photos": true,
"assignees": true,
"assetOwner": true,
"reportedBy": true,
"operatorNotes": true,
"visualId": true,
"barcode": true,
"qrCode": true
},
"plannedMaintenanceForm": {
"description": true,
"generateTicketPerAsset": true,
"isActive": true,
"interval": true,
"duration": true,
"assignees": true
},
"dashboard": {
"autoRefresh": false,
"autoRefreshInterval": 5
}
},
"created": "2025-01-01T00:00:00.000Z",
"updated": "2025-01-01T00:00:00.000Z"
}
Features:
- Auto-creates default settings if they don't exist
- Returns settings immediately (no need to check if they exist first)
- All authenticated users with account access can view settings
2. Update Account Settings
PATCH /v1/accounts/{accountId}/settings
Authorization: IDTOKEN.<firebaseIdToken>
Content-Type: application/json
Request Body:
{
"settings": {
"ticketStatuses": {
"SKIPPED": false
},
"formSettings": {
"dashboard": {
"autoRefresh": true,
"autoRefreshInterval": 10
}
}
}
}
Response (200 OK): Returns the updated settings object (same format as GET)
Features:
- Partial updates: Only include fields you want to change
- Deep merge: Nested objects are merged, not replaced
- Role-based access: Requires OWNER, ADMIN, or SITE_MANAGER role
- Auto-creates default settings if they don't exist before updating
Important: The request body must be wrapped in a settings object.
Settings Structure
Ticket Statuses
Controls which ticket statuses are available/enabled:
OPENIN_PROGRESSCOMPLETEDCLOSEDSKIPPED
Form Settings
Breakdown Form (Desktop - maintor-app)
Controls visibility of fields in breakdown maintenance tickets for desktop:
title,priority,status,description,breakdownEndedlaborEntries,rootCause,problemDescription,solutionDescriptiondowntime,photos,assignees,assetOwner,reportedBy,operatorNotesvisualId,barcode,qrCode
Breakdown Form (Mobile - maintor-eng)
Controls visibility of fields in breakdown maintenance tickets for mobile:
title,priority,status,description,breakdownEndedlaborEntries,rootCause,problemDescription,solutionDescriptiondowntime,photos,assignees,assetOwner,reportedBy,operatorNotesvisualId,barcode,qrCode
Note: Desktop and mobile forms have separate visibility controls, allowing different field configurations for each platform.
Planned Maintenance Form
Controls visibility of fields in planned maintenance tickets:
description,generateTicketPerAsset,isActiveinterval,duration,assignees
Dashboard
Dashboard behavior settings:
autoRefresh(boolean): Enable automatic refreshautoRefreshInterval(number): Refresh interval in minutes (1-60)
Implementation Examples
JavaScript/Vue.js Example
import { getAuth } from 'firebase/auth';
// Base API URL
const API_BASE_URL = 'https://api.maintor.systems'; // or your dev URL
/**
* Get account settings from backend
*/
async function getAccountSettings(accountId) {
try {
const auth = getAuth();
const user = auth.currentUser;
if (!user) {
throw new Error('User must be authenticated');
}
const idToken = await user.getIdToken();
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId}/settings`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `IDTOKEN.${idToken}`
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to get settings');
}
const settings = await response.json();
return settings;
} catch (error) {
console.error('Error fetching settings:', error);
throw error;
}
}
/**
* Update account settings (partial update)
*/
async function updateAccountSettings(accountId, updates) {
try {
const auth = getAuth();
const user = auth.currentUser;
if (!user) {
throw new Error('User must be authenticated');
}
const idToken = await user.getIdToken();
// Wrap updates in settings object
const body = {
settings: updates
};
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId}/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `IDTOKEN.${idToken}`
},
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to update settings');
}
const updatedSettings = await response.json();
return updatedSettings;
} catch (error) {
console.error('Error updating settings:', error);
throw error;
}
}
// Usage examples:
// Get all settings
const settings = await getAccountSettings('account_123');
// Update ticket statuses
await updateAccountSettings('account_123', {
ticketStatuses: {
SKIPPED: false
}
});
// Update dashboard settings
await updateAccountSettings('account_123', {
formSettings: {
dashboard: {
autoRefresh: true,
autoRefreshInterval: 10
}
}
});
// Update breakdown form field visibility (desktop)
await updateAccountSettings('account_123', {
formSettings: {
breakdownFormDesktop: {
operatorNotes: false,
downtime: false
}
}
});
// Update breakdown form field visibility (mobile)
await updateAccountSettings('account_123', {
formSettings: {
breakdownFormMobile: {
visualId: true,
barcode: true,
qrCode: true
}
}
});
React Hook Example
import { useState, useEffect, useCallback } from 'react';
import { getAuth } from 'firebase/auth';
const API_BASE_URL = 'https://api.maintor.systems';
export function useAccountSettings(accountId) {
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch settings
const fetchSettings = useCallback(async () => {
if (!accountId) return;
setLoading(true);
setError(null);
try {
const auth = getAuth();
const user = auth.currentUser;
const idToken = await user.getIdToken();
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId}/settings`, {
headers: {
'Authorization': `IDTOKEN.${idToken}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch settings');
}
const data = await response.json();
setSettings(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [accountId]);
// Update settings
const updateSettings = useCallback(async (updates) => {
if (!accountId) return;
try {
const auth = getAuth();
const user = auth.currentUser;
const idToken = await user.getIdToken();
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId}/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `IDTOKEN.${idToken}`
},
body: JSON.stringify({
settings: updates
})
});
if (!response.ok) {
throw new Error('Failed to update settings');
}
const updated = await response.json();
setSettings(updated);
return updated;
} catch (err) {
setError(err.message);
throw err;
}
}, [accountId]);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
return {
settings,
loading,
error,
updateSettings,
refetch: fetchSettings
};
}
// Usage in component:
function SettingsPage({ accountId }) {
const { settings, loading, error, updateSettings } = useAccountSettings(accountId);
const handleToggleAutoRefresh = async () => {
await updateSettings({
formSettings: {
dashboard: {
autoRefresh: !settings.formSettings.dashboard.autoRefresh
}
}
});
};
if (loading) return <div>Loading settings...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<label>
<input
type="checkbox"
checked={settings.formSettings.dashboard.autoRefresh}
onChange={handleToggleAutoRefresh}
/>
Auto-refresh dashboard
</label>
</div>
);
}
Vue.js Composable Example
import { ref, computed } from 'vue';
import { getAuth } from 'firebase/auth';
const API_BASE_URL = 'https://api.maintor.systems';
export function useAccountSettings(accountId) {
const settings = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchSettings = async () => {
if (!accountId.value) return;
loading.value = true;
error.value = null;
try {
const auth = getAuth();
const user = auth.currentUser;
const idToken = await user.getIdToken();
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId.value}/settings`, {
headers: {
'Authorization': `IDTOKEN.${idToken}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch settings');
}
settings.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
const updateSettings = async (updates) => {
if (!accountId.value) return;
try {
const auth = getAuth();
const user = auth.currentUser;
const idToken = await user.getIdToken();
const response = await fetch(`${API_BASE_URL}/v1/accounts/${accountId.value}/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `IDTOKEN.${idToken}`
},
body: JSON.stringify({
settings: updates
})
});
if (!response.ok) {
throw new Error('Failed to update settings');
}
settings.value = await response.json();
return settings.value;
} catch (err) {
error.value = err.message;
throw err;
}
};
return {
settings,
loading,
error,
updateSettings,
fetchSettings
};
}
Migration Strategy
Step 1: Replace localStorage Reads
Before (localStorage):
// Old way
const settings = JSON.parse(localStorage.getItem('accountSettings') || '{}');
const autoRefresh = settings.dashboard?.autoRefresh ?? false;
After (Backend API):
// New way
const settings = await getAccountSettings(accountId);
const autoRefresh = settings.formSettings?.dashboard?.autoRefresh ?? false;
Step 2: Replace localStorage Writes
Before (localStorage):
// Old way
const currentSettings = JSON.parse(localStorage.getItem('accountSettings') || '{}');
const updatedSettings = {
...currentSettings,
dashboard: {
...currentSettings.dashboard,
autoRefresh: true
}
};
localStorage.setItem('accountSettings', JSON.stringify(updatedSettings));
After (Backend API):
// New way
await updateAccountSettings(accountId, {
formSettings: {
dashboard: {
autoRefresh: true
}
}
});
Step 3: Handle Initial Load
Option A: Load on App Start
// In your app initialization
async function initializeApp(accountId) {
try {
const settings = await getAccountSettings(accountId);
// Store in your state management (Vuex, Pinia, Redux, etc.)
store.commit('setSettings', settings);
} catch (error) {
console.error('Failed to load settings:', error);
// Fallback to defaults or show error
}
}
Option B: Load on Demand
// Load settings when needed
const settings = computed(() => {
if (!store.state.settings) {
// Trigger fetch
getAccountSettings(accountId).then(settings => {
store.commit('setSettings', settings);
});
return null; // or return defaults
}
return store.state.settings;
});
Step 4: Migration Helper (One-Time)
If you have existing localStorage data, create a one-time migration function:
async function migrateSettingsFromLocalStorage(accountId) {
// Check if migration already done
const migrationKey = `settings_migrated_${accountId}`;
if (localStorage.getItem(migrationKey)) {
return; // Already migrated
}
// Get old settings from localStorage
const oldSettings = JSON.parse(localStorage.getItem('accountSettings') || '{}');
if (Object.keys(oldSettings).length === 0) {
// No old settings to migrate
localStorage.setItem(migrationKey, 'true');
return;
}
try {
// Map old structure to new structure
// If old settings had breakdownForm, apply to both desktop and mobile
const oldBreakdownForm = oldSettings.breakdownForm || {};
const newSettings = {
ticketStatuses: oldSettings.ticketStatuses || {},
formSettings: {
breakdownFormDesktop: oldBreakdownForm,
breakdownFormMobile: oldBreakdownForm, // Copy same settings to mobile initially
plannedMaintenanceForm: oldSettings.plannedMaintenanceForm || {},
dashboard: oldSettings.dashboard || {
autoRefresh: false,
autoRefreshInterval: 5
}
}
};
// Upload to backend
await updateAccountSettings(accountId, newSettings);
// Mark as migrated
localStorage.setItem(migrationKey, 'true');
// Optional: Clear old localStorage data after successful migration
// localStorage.removeItem('accountSettings');
console.log('Settings migrated successfully');
} catch (error) {
console.error('Failed to migrate settings:', error);
// Don't mark as migrated, so it can retry
}
}
Error Handling
Common Errors
401 Unauthorized:
if (response.status === 401) {
// User not authenticated - redirect to login
router.push('/login');
}
403 Forbidden:
if (response.status === 403) {
// User doesn't have permission
// Show error message or disable settings UI
showError('You do not have permission to modify settings');
}
404 Not Found:
if (response.status === 404) {
// Account not found
// This shouldn't happen if accountId is correct
showError('Account not found');
}
500 Internal Server Error:
if (response.status === 500) {
// Server error - retry or show error
showError('Server error. Please try again later.');
}
Retry Logic
async function getAccountSettingsWithRetry(accountId, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await getAccountSettings(accountId);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
Best Practices
1. Cache Settings Locally (Optional)
You can still cache settings in memory/state for performance, but don't persist to localStorage:
// Good: Cache in memory/state
const [settings, setSettings] = useState(null);
// Bad: Don't use localStorage anymore
// localStorage.setItem('settings', JSON.stringify(settings));
2. Debounce Updates
If users can change settings rapidly, debounce the API calls:
import { debounce } from 'lodash';
const debouncedUpdate = debounce(async (accountId, updates) => {
await updateAccountSettings(accountId, updates);
}, 500);
// Usage
debouncedUpdate(accountId, { formSettings: { dashboard: { autoRefresh: true } } });
3. Optimistic Updates
Update UI immediately, then sync with backend:
async function updateSettingsOptimistic(accountId, updates) {
// Update local state immediately
const optimisticSettings = { ...currentSettings, ...updates };
setSettings(optimisticSettings);
try {
// Sync with backend
const actualSettings = await updateAccountSettings(accountId, updates);
setSettings(actualSettings);
} catch (error) {
// Revert on error
setSettings(currentSettings);
showError('Failed to save settings');
}
}
4. Handle Offline Mode
async function getAccountSettingsWithFallback(accountId) {
try {
return await getAccountSettings(accountId);
} catch (error) {
// If offline, use cached version or defaults
const cached = sessionStorage.getItem(`settings_cache_${accountId}`);
if (cached) {
return JSON.parse(cached);
}
// Return defaults
return getDefaultSettings();
}
}
5. Settings Structure Differences
Important: Note the structure differences:
-
Old localStorage:
settings.dashboard.autoRefresh -
New API:
settings.formSettings.dashboard.autoRefresh -
Old localStorage:
settings.breakdownForm.priority -
New API:
- Desktop:
settings.formSettings.breakdownFormDesktop.priority - Mobile:
settings.formSettings.breakdownFormMobile.priority
- Desktop:
Make sure to update all references to use the new structure. The breakdown form settings are now split into separate desktop and mobile configurations.
Testing
Test Cases
- Get settings for new account (should return defaults)
- Update partial settings (should merge, not replace)
- Update nested settings (should deep merge)
- Handle authentication errors
- Handle permission errors (403)
- Handle network errors
Example Test
describe('Account Settings', () => {
it('should fetch default settings for new account', async () => {
const settings = await getAccountSettings('new_account_id');
expect(settings.ticketStatuses).toBeDefined();
expect(settings.formSettings).toBeDefined();
});
it('should update settings partially', async () => {
await updateAccountSettings('account_id', {
formSettings: {
dashboard: { autoRefresh: true }
}
});
const updated = await getAccountSettings('account_id');
expect(updated.formSettings.dashboard.autoRefresh).toBe(true);
// Other settings should remain unchanged
expect(updated.ticketStatuses).toBeDefined();
});
});
Checklist
- Replace all
localStorage.getItem('accountSettings')calls with API calls - Replace all
localStorage.setItem('accountSettings', ...)calls with API calls - Update all references to use new structure (
formSettings.dashboardinstead ofdashboard) - Update breakdown form references: use
breakdownFormDesktopfor desktop (maintor-app) andbreakdownFormMobilefor mobile (maintor-eng) - Add error handling for API calls
- Test with new accounts (should get defaults with both desktop and mobile breakdown forms)
- Test partial updates (updating only desktop or only mobile breakdown form)
- Test permission errors (non-admin users)
- Remove localStorage migration code after migration is complete
- Update any documentation that references localStorage settings
Support
If you encounter issues:
- Check the API response in browser DevTools Network tab
- Verify the accountId is correct
- Verify the user has the required permissions
- Check server logs for detailed error messages
For API documentation, see: openapi.json (search for /v1/accounts/{accountId}/settings)