Add facilities, contracts, orders, value lists, audit log, and desktop app modules

Extends omsorgCore with full CRUD for Facility/Contract/Order plus
configurable value lists and an audit trail, and wires the omsorgapp
frontend up to the new facilities, settings, and audit-log modules;
includes a sidebar active-nav-item highlight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-08 23:29:16 +02:00
co-authored by Claude Sonnet 5
parent ee74ed65f5
commit e9e96a57dc
528 changed files with 96814 additions and 456 deletions
+98 -54
View File
@@ -1,10 +1,15 @@
const { app, BrowserWindow, ipcMain, shell, safeStorage } = require('electron');
const { app, BrowserWindow, ipcMain, safeStorage } = require('electron');
const path = require('path');
const fs = require('fs');
const authClient = require('./backend/authClient.cjs');
const httpClient = require('./backend/httpClient.cjs');
const employeesClient = require('./backend/employeesClient.cjs');
const facilitiesClient = require('./backend/facilitiesClient.cjs');
const facilityContactsClient = require('./backend/facilityContactsClient.cjs');
const usersClient = require('./backend/usersClient.cjs');
const rolesClient = require('./backend/rolesClient.cjs');
const auditLogClient = require('./backend/auditLogClient.cjs');
const valueListsClient = require('./backend/valueListsClient.cjs');
app.commandLine.appendSwitch('lang', 'de');
@@ -144,44 +149,6 @@ function authorizedRequest(method, resourcePath, body) {
return withAuthRetry((accessToken) => httpClient.request(method, resourcePath, { body, accessToken }));
}
function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }
function appRoot() {
const base = path.join(app.getPath('documents'), 'Omsorg Business Controls Pro');
ensureDir(base);
ensureDir(path.join(base, 'database'));
ensureDir(path.join(base, 'documents'));
ensureDir(path.join(base, 'documents', 'employees'));
ensureDir(path.join(base, 'documents', 'customers'));
ensureDir(path.join(base, 'backups'));
return base;
}
function dbPath() { return path.join(appRoot(), 'database', 'omsorg-local-db.json'); }
function defaultData() {
return {
version: '0.1.1',
createdAt: new Date().toISOString(),
users: [
{ id:'u1', name:'Sabina Dautovic', role:'Geschäftsführung' },
{ id:'u2', name:'Malik Neumann', role:'Geschäftsführung' },
{ id:'u3', name:'Sabrina Berggötz', role:'Disposition' }
],
employees: [
{ id:'e1', name:'Dirk Artmann', qualification:'3-jährige Pflegefachkraft', status:'Aktiv' },
{ id:'e2', name:'Carina Vetter', qualification:'3-jährige Pflegefachkraft', status:'Aktiv' },
{ id:'e3', name:'Dilara Anders', qualification:'1-jährige Pflegekraft', status:'Aktiv' },
{ id:'e4', name:'Pascal Galliot', qualification:'1-jährige Pflegekraft', status:'Aktiv' }
],
customers: [],
assignments: [],
notes: []
};
}
function readDb() {
const file = dbPath();
if (!fs.existsSync(file)) fs.writeFileSync(file, JSON.stringify(defaultData(), null, 2));
return JSON.parse(fs.readFileSync(file, 'utf-8'));
}
function writeDb(data) { fs.writeFileSync(dbPath(), JSON.stringify(data, null, 2)); return data; }
function createWindow() {
mainWindow = new BrowserWindow({
width: 1440,
@@ -200,7 +167,6 @@ function createWindow() {
else mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
app.whenReady().then(async () => {
appRoot();
createWindow();
await bootstrapSession();
broadcastSession();
@@ -208,18 +174,6 @@ app.whenReady().then(async () => {
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
ipcMain.handle('db:get', () => readDb());
ipcMain.handle('db:set', (_event, data) => writeDb(data));
ipcMain.handle('app:paths', () => ({ root: appRoot(), db: dbPath() }));
ipcMain.handle('app:openRoot', () => shell.openPath(appRoot()));
ipcMain.handle('backup:create', () => {
const root = appRoot();
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(root, 'backups', `backup-${stamp}.json`);
fs.copyFileSync(dbPath(), backupFile);
return backupFile;
});
ipcMain.handle('auth:login', async (_event, { username, password }) => {
const result = await authClient.login(username, password);
if (!result.ok) {
@@ -263,7 +217,17 @@ ipcMain.handle('auth:verifyPasswordResetCode', async (_event, { username, pin })
ipcMain.handle('auth:resetPassword', async (_event, { resetToken, newPassword }) => {
const result = await authClient.resetPassword(resetToken, newPassword);
return { success: result.ok };
if (!result.ok) {
const error = result.status === 400
? 'password_too_short'
: 'invalid_or_expired';
return {
success: false,
error,
message: result.status === 400 && typeof result.data === 'string' ? result.data : undefined
};
}
return { success: true };
});
// Erfolgreicher Wechsel widerruft serverseitig alle Sessions (siehe UserService.ChangeOwnPasswordAsync) -
@@ -272,12 +236,27 @@ ipcMain.handle('auth:changePassword', async (_event, { currentPassword, newPassw
if (!session.accessToken) return { success: false, error: 'not_authenticated' };
const result = await authClient.changePassword(session.accessToken, currentPassword, newPassword);
if (!result.ok) {
return { success: false, error: result.status === 401 ? 'invalid_current_password' : 'unknown' };
const error = result.status === 401
? 'invalid_current_password'
: result.status === 400
? 'password_too_short'
: 'unknown';
return {
success: false,
error,
message: result.status === 400 && typeof result.data === 'string' ? result.data : undefined
};
}
clearSession();
return { success: true };
});
ipcMain.handle('auth:getPasswordPolicy', async () => {
const result = await authClient.getPasswordPolicy();
if (!result.ok) return { success: false };
return { success: true, minLength: result.minLength };
});
// Generischer Proxy für Ressourcen ohne eigene <kategorie>Client.cjs-Datei (siehe Plan).
ipcMain.handle('api:get', (_event, resourcePath) => authorizedRequest('GET', resourcePath));
ipcMain.handle('api:post', (_event, resourcePath, body) => authorizedRequest('POST', resourcePath, body));
@@ -290,7 +269,72 @@ ipcMain.handle('employees:update', (_event, id, payload) =>
withAuthRetry((accessToken) => employeesClient.updateEmployee(accessToken, id, payload))
);
ipcMain.handle('facilities:list', (_event, params) => withAuthRetry((accessToken) => facilitiesClient.listFacilities(accessToken, params)));
ipcMain.handle('facilities:create', (_event, payload) =>
withAuthRetry((accessToken) => facilitiesClient.createFacility(accessToken, payload))
);
ipcMain.handle('facilities:update', (_event, id, payload) =>
withAuthRetry((accessToken) => facilitiesClient.updateFacility(accessToken, id, payload))
);
ipcMain.handle('facilityContacts:list', (_event, facilityId) =>
withAuthRetry((accessToken) => facilityContactsClient.listFacilityContacts(accessToken, facilityId))
);
ipcMain.handle('facilityContacts:create', (_event, facilityId, payload) =>
withAuthRetry((accessToken) => facilityContactsClient.createFacilityContact(accessToken, facilityId, payload))
);
ipcMain.handle('facilityContacts:update', (_event, facilityId, id, payload) =>
withAuthRetry((accessToken) => facilityContactsClient.updateFacilityContact(accessToken, facilityId, id, payload))
);
ipcMain.handle('users:list', () => withAuthRetry((accessToken) => usersClient.listUsers(accessToken)));
ipcMain.handle('users:create', (_event, payload) =>
withAuthRetry((accessToken) => usersClient.createUser(accessToken, payload))
);
ipcMain.handle('users:listPermissionOverrides', (_event, userId) =>
withAuthRetry((accessToken) => usersClient.listPermissionOverrides(accessToken, userId))
);
ipcMain.handle('users:addPermissionOverride', (_event, userId, payload) =>
withAuthRetry((accessToken) => usersClient.addPermissionOverride(accessToken, userId, payload))
);
ipcMain.handle('users:deletePermissionOverride', (_event, userId, overrideId) =>
withAuthRetry((accessToken) => usersClient.deletePermissionOverride(accessToken, userId, overrideId))
);
ipcMain.handle('roles:list', () => withAuthRetry((accessToken) => rolesClient.listRoles(accessToken)));
ipcMain.handle('roles:get', (_event, roleId) =>
withAuthRetry((accessToken) => rolesClient.getRole(accessToken, roleId))
);
ipcMain.handle('roles:create', (_event, payload) =>
withAuthRetry((accessToken) => rolesClient.createRole(accessToken, payload))
);
ipcMain.handle('roles:updatePermissions', (_event, roleId, payload) =>
withAuthRetry((accessToken) => rolesClient.updateRolePermissions(accessToken, roleId, payload))
);
ipcMain.handle('auditLog:list', (_event, params) =>
withAuthRetry((accessToken) => auditLogClient.listAuditLog(accessToken, params))
);
ipcMain.handle('valueLists:list', () => withAuthRetry((accessToken) => valueListsClient.listValueLists(accessToken)));
ipcMain.handle('valueLists:listItems', (_event, key) =>
withAuthRetry((accessToken) => valueListsClient.listItems(accessToken, key))
);
ipcMain.handle('valueLists:createItem', (_event, key, payload) =>
withAuthRetry((accessToken) => valueListsClient.createItem(accessToken, key, payload))
);
ipcMain.handle('valueLists:updateItem', (_event, key, id, payload) =>
withAuthRetry((accessToken) => valueListsClient.updateItem(accessToken, key, id, payload))
);
ipcMain.handle('valueLists:deleteItem', (_event, key, id) =>
withAuthRetry((accessToken) => valueListsClient.deleteItem(accessToken, key, id))
);
ipcMain.handle('valueLists:getUsages', (_event, key, id) =>
withAuthRetry((accessToken) => valueListsClient.getUsages(accessToken, key, id))
);
ipcMain.handle('valueLists:listTransitions', (_event, key) =>
withAuthRetry((accessToken) => valueListsClient.listTransitions(accessToken, key))
);
ipcMain.handle('valueLists:replaceTransitions', (_event, key, payload) =>
withAuthRetry((accessToken) => valueListsClient.replaceTransitions(accessToken, key, payload))
);