Benutzerverwaltung mit Rollen/Rechten und Urlaubsplanung

Führt Login mit vier Rollen (Admin, Teamleiter, Mitarbeiter, Beobachter) ein,
über die Teammitglieder eigenen Urlaub inkl. Pflicht-Vertreter eintragen
können. Die Auslastungsberechnung berücksichtigt Urlaub jetzt als reduzierte
Kapazität. Neue Konten vergeben ihr Passwort selbst beim ersten Login.
Migrationsskript für bestehende Mitarbeiter aus der Vorversion inklusive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:57:25 +02:00
parent 60a0928d2f
commit 06ea149d41
20 changed files with 1139 additions and 91 deletions
+57 -32
View File
@@ -1,47 +1,72 @@
<script setup>
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { store } from './store.js';
import LoginPanel from './LoginPanel.vue';
import SchedulePanel from './SchedulePanel.vue';
import UtilizationPanel from './UtilizationPanel.vue';
import VacationPanel from './VacationPanel.vue';
import ProjectsPanel from './ProjectsPanel.vue';
import EmployeesPanel from './EmployeesPanel.vue';
import TeamsPanel from './TeamsPanel.vue';
import UsersPanel from './UsersPanel.vue';
const tabs = [
{ id: 'schedule', label: 'Zeitplan' },
{ id: 'utilization', label: 'Auslastung' },
{ id: 'projects', label: 'Projekte' },
{ id: 'employees', label: 'Mitarbeiter' },
{ id: 'teams', label: 'Teams' },
const ALL_TABS = [
{ id: 'schedule', label: 'Zeitplan', roles: ['admin', 'teamleiter', 'mitarbeiter', 'beobachter'] },
{ id: 'utilization', label: 'Auslastung', roles: ['admin', 'teamleiter', 'mitarbeiter', 'beobachter'] },
{ id: 'vacation', label: 'Urlaub', roles: ['admin', 'teamleiter', 'mitarbeiter', 'beobachter'] },
{ id: 'projects', label: 'Projekte', roles: ['admin', 'teamleiter', 'mitarbeiter'] },
{ id: 'employees', label: 'Mitarbeiter', roles: ['admin', 'teamleiter', 'mitarbeiter'] },
{ id: 'teams', label: 'Teams', roles: ['admin', 'teamleiter', 'mitarbeiter'] },
{ id: 'users', label: 'Benutzer', roles: ['admin'] },
];
const tabs = computed(() => {
const role = store.session.user?.role;
return ALL_TABS.filter((t) => t.roles.includes(role));
});
const activeTab = ref('schedule');
onMounted(() => store.loadAll());
onMounted(async () => {
await store.restoreSession();
if (store.session.user) await store.loadAll();
});
async function logout() {
await store.logout();
activeTab.value = 'schedule';
}
</script>
<template>
<header class="topbar">
<h1>Ressourcenplanung</h1>
<nav class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</nav>
</header>
<main>
<p v-if="store.loading" class="muted">Lade Daten</p>
<p v-if="store.error" class="muted" style="color: #ef4444">{{ store.error }}</p>
<template v-else>
<SchedulePanel v-if="activeTab === 'schedule'" />
<UtilizationPanel v-if="activeTab === 'utilization'" />
<ProjectsPanel v-if="activeTab === 'projects'" />
<EmployeesPanel v-if="activeTab === 'employees'" />
<TeamsPanel v-if="activeTab === 'teams'" />
</template>
</main>
<LoginPanel v-if="!store.session.user" />
<template v-else>
<header class="topbar">
<h1>Ressourcenplanung</h1>
<nav class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</nav>
<span class="muted">{{ store.session.user.email }} ({{ store.session.user.role }})</span>
<button class="btn secondary" @click="logout">Abmelden</button>
</header>
<main>
<p v-if="store.loading" class="muted">Lade Daten</p>
<p v-if="store.error" class="muted" style="color: #ef4444">{{ store.error }}</p>
<template v-else>
<SchedulePanel v-if="activeTab === 'schedule'" />
<UtilizationPanel v-if="activeTab === 'utilization'" />
<VacationPanel v-if="activeTab === 'vacation'" />
<ProjectsPanel v-if="activeTab === 'projects'" />
<EmployeesPanel v-if="activeTab === 'employees'" />
<TeamsPanel v-if="activeTab === 'teams'" />
<UsersPanel v-if="activeTab === 'users'" />
</template>
</main>
</template>
</template>
+7 -4
View File
@@ -1,7 +1,9 @@
<script setup>
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import { store } from './store.js';
const canManage = computed(() => ['admin', 'teamleiter'].includes(store.session.user?.role));
const newEmployee = reactive({ first_name: '', last_name: '', email: '', role: '', weekly_capacity_hours: 40 });
async function addEmployee() {
@@ -36,7 +38,7 @@ async function saveEdit(id) {
</script>
<template>
<div class="panel-section">
<div class="panel-section" v-if="canManage">
<h2>Neuer Mitarbeiter</h2>
<div class="form-row">
<input v-model="newEmployee.first_name" placeholder="Vorname" />
@@ -63,7 +65,7 @@ async function saveEdit(id) {
</thead>
<tbody>
<tr v-for="e in store.employees" :key="e.id">
<template v-if="editingId === e.id">
<template v-if="editingId === e.id && canManage">
<td><input v-model="editForm.first_name" style="width: 100%" /></td>
<td><input v-model="editForm.last_name" style="width: 100%" /></td>
<td><input v-model="editForm.email" style="width: 100%" /></td>
@@ -80,10 +82,11 @@ async function saveEdit(id) {
<td>{{ e.email }}</td>
<td>{{ e.role }}</td>
<td>{{ e.weekly_capacity_hours }}</td>
<td>
<td v-if="canManage">
<button class="btn secondary" @click="startEdit(e)">Bearbeiten</button>
<button class="btn danger" @click="store.deleteEmployee(e.id)">Löschen</button>
</td>
<td v-else></td>
</template>
</tr>
</tbody>
+38
View File
@@ -0,0 +1,38 @@
<script setup>
import { reactive, ref } from 'vue';
import { store } from './store.js';
const form = reactive({ email: '', password: '' });
const error = ref(null);
const loading = ref(false);
async function submit() {
if (!form.email || !form.password) return;
loading.value = true;
error.value = null;
try {
await store.login(form.email, form.password);
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="panel-section" style="max-width: 360px; margin: 64px auto">
<h2>Anmelden</h2>
<p class="muted">
Neue Konten haben noch kein Passwort: das beim ersten Login eingegebene Passwort wird übernommen.
</p>
<form @submit.prevent="submit">
<div class="form-row" style="flex-direction: column; align-items: stretch">
<input v-model="form.email" type="email" placeholder="E-Mail" autocomplete="username" />
<input v-model="form.password" type="password" placeholder="Passwort" autocomplete="current-password" />
<button class="btn" type="submit" :disabled="loading">{{ loading ? 'Anmelden…' : 'Anmelden' }}</button>
</div>
</form>
<p v-if="error" class="muted" style="color: #ef4444">{{ error }}</p>
</div>
</template>
+21 -6
View File
@@ -1,8 +1,18 @@
<script setup>
import { reactive } from 'vue';
import { computed, reactive } from 'vue';
import { store } from './store.js';
import { formatDate, fullName } from './helpers.js';
const canManage = computed(() => ['admin', 'teamleiter'].includes(store.session.user?.role));
function isAssignedToMe(task) {
return task.assignments.some((a) => a.employee_id === store.session.user?.employee_id);
}
function statusEditable(task) {
return canManage.value || isAssignedToMe(task);
}
const newProject = reactive({ name: '', description: '', start_date: '', end_date: '', color: '#4f46e5' });
const newTask = reactive({});
const newAssignment = reactive({});
@@ -60,7 +70,7 @@ async function duplicateTask(projectId, taskId) {
</script>
<template>
<div class="panel-section">
<div class="panel-section" v-if="canManage">
<h2>Neues Projekt</h2>
<div class="form-row">
<input v-model="newProject.name" placeholder="Projektname" />
@@ -75,7 +85,7 @@ async function duplicateTask(projectId, taskId) {
<div class="panel-section" v-for="project in store.projects" :key="project.id">
<div style="display: flex; justify-content: space-between; align-items: center">
<h2 :style="{ color: project.color }">{{ project.name }}</h2>
<button class="btn secondary" @click="expanded[project.id] = !expanded[project.id]">
<button v-if="canManage" class="btn secondary" @click="expanded[project.id] = !expanded[project.id]">
{{ expanded[project.id] ? 'Zuklappen' : 'Aufgaben verwalten' }}
</button>
</div>
@@ -98,14 +108,17 @@ async function duplicateTask(projectId, taskId) {
<td>{{ formatDate(task.start_date) }}</td>
<td>
<input
v-if="canManage"
type="date"
:value="task.end_date"
@change="store.updateTask(project.id, task.id, { end_date: $event.target.value })"
/>
<span v-else>{{ formatDate(task.end_date) }}</span>
</td>
<td>{{ task.estimated_hours }}</td>
<td>
<select
v-if="statusEditable(task)"
:value="task.status"
@change="store.updateTask(project.id, task.id, { status: $event.target.value })"
>
@@ -113,17 +126,19 @@ async function duplicateTask(projectId, taskId) {
<option value="in Arbeit">in Arbeit</option>
<option value="erledigt">erledigt</option>
</select>
<span v-else>{{ task.status }}</span>
</td>
<td>
<span v-for="a in task.assignments" :key="a.id" class="tag">
{{ a.employee_name }} ({{ a.allocated_hours_per_week }}h)
<button
v-if="canManage"
class="tag-remove"
title="Zuweisung entfernen"
@click="store.unassignEmployee(project.id, task.id, a.id)"
>×</button>
</span>
<div class="assign-inline">
<div v-if="canManage" class="assign-inline">
<select v-model="assignmentForm(task.id).employee_id">
<option value="" disabled>Mitarbeiter wählen</option>
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ fullName(e) }}</option>
@@ -136,7 +151,7 @@ async function duplicateTask(projectId, taskId) {
</tbody>
</table>
<template v-if="expanded[project.id]">
<template v-if="expanded[project.id] && canManage">
<h3>Neue Aufgabe</h3>
<div class="form-row">
<input v-model="taskForm(project.id).name" placeholder="Aufgabenname" />
@@ -167,7 +182,7 @@ async function duplicateTask(projectId, taskId) {
</div>
</template>
<div style="margin-top: 12px">
<div v-if="canManage" style="margin-top: 12px">
<button class="btn danger" @click="store.deleteProject(project.id)">Projekt löschen</button>
</div>
</div>
+6 -4
View File
@@ -1,8 +1,10 @@
<script setup>
import { reactive } from 'vue';
import { computed, reactive } from 'vue';
import { store } from './store.js';
import { fullName } from './helpers.js';
const canManage = computed(() => ['admin', 'teamleiter'].includes(store.session.user?.role));
const newTeam = reactive({ name: '', description: '' });
const newMember = reactive({});
@@ -26,7 +28,7 @@ async function addMember(teamId) {
</script>
<template>
<div class="panel-section">
<div class="panel-section" v-if="canManage">
<h2>Neues Team</h2>
<div class="form-row">
<input v-model="newTeam.name" placeholder="Teamname" />
@@ -41,11 +43,11 @@ async function addMember(teamId) {
<span v-for="m in team.members" :key="m.id" class="tag">
{{ fullName(m) }}
<a href="#" @click.prevent="store.removeTeamMember(team.id, m.id)" style="margin-left: 4px">×</a>
<a v-if="canManage" href="#" @click.prevent="store.removeTeamMember(team.id, m.id)" style="margin-left: 4px">×</a>
</span>
<p v-if="team.members.length === 0" class="muted">Keine Mitglieder.</p>
<div class="form-row">
<div v-if="canManage" class="form-row">
<select v-model="memberForm(team.id)[team.id]">
<option value="" disabled>Mitarbeiter hinzufügen</option>
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ fullName(e) }}</option>
+87
View File
@@ -0,0 +1,87 @@
<script setup>
import { computed, reactive, ref } from 'vue';
import { store } from './store.js';
const ROLES = [
{ value: 'admin', label: 'Admin' },
{ value: 'teamleiter', label: 'Teamleiter' },
{ value: 'mitarbeiter', label: 'Mitarbeiter' },
{ value: 'beobachter', label: 'Beobachter' },
];
const availableEmployees = computed(() =>
store.employees.filter((e) => !store.users.some((u) => u.employee_id === e.id))
);
const newUser = reactive({ employee_id: '', email: '', role: 'mitarbeiter' });
const createError = ref(null);
function onEmployeeChange() {
const employee = store.employees.find((e) => e.id === Number(newUser.employee_id));
if (employee?.email) newUser.email = employee.email;
}
async function addUser() {
createError.value = null;
if (!newUser.email || !newUser.role) return;
try {
await store.createUser({ ...newUser, employee_id: newUser.employee_id || null });
Object.assign(newUser, { employee_id: '', email: '', role: 'mitarbeiter' });
} catch (e) {
createError.value = e.message;
}
}
async function resetPassword(id) {
await store.updateUser(id, { reset_password: true });
}
</script>
<template>
<div class="panel-section">
<h2>Neuer Benutzer</h2>
<div class="form-row">
<select v-model.number="newUser.employee_id" @change="onEmployeeChange">
<option value="">(kein Mitarbeiterbezug)</option>
<option v-for="e in availableEmployees" :key="e.id" :value="e.id">{{ e.first_name }} {{ e.last_name }}</option>
</select>
<input v-model="newUser.email" placeholder="E-Mail" />
<select v-model="newUser.role">
<option v-for="r in ROLES" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
<button class="btn" @click="addUser">Anlegen</button>
</div>
<p class="muted">Das Passwort wird von der Person selbst beim ersten Login vergeben.</p>
<p v-if="createError" class="muted" style="color: #ef4444">{{ createError }}</p>
</div>
<div class="panel-section">
<h2>Benutzer</h2>
<table>
<thead>
<tr>
<th>Mitarbeiter</th>
<th>E-Mail</th>
<th>Rolle</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="u in store.users" :key="u.id">
<td>{{ u.employee_name || '—' }}</td>
<td>{{ u.email }}</td>
<td>
<select :value="u.role" @change="store.updateUser(u.id, { role: $event.target.value })">
<option v-for="r in ROLES" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
</td>
<td>
<button class="btn secondary" @click="resetPassword(u.id)">Passwort zurücksetzen</button>
<button class="btn danger" @click="store.deleteUser(u.id)">Löschen</button>
</td>
</tr>
</tbody>
</table>
<p v-if="store.users.length === 0" class="muted">Noch keine Benutzer angelegt.</p>
</div>
</template>
+3 -3
View File
@@ -30,10 +30,10 @@ function percent(hours, capacity) {
<td v-for="(hours, idx) in employee.hours" :key="idx">
<div
class="heatmap-cell"
:style="{ background: utilizationColor(percent(hours, employee.weekly_capacity_hours)) }"
:title="`${hours}h von ${employee.weekly_capacity_hours}h`"
:style="{ background: utilizationColor(percent(hours, employee.capacity[idx])) }"
:title="`${hours}h von ${employee.capacity[idx]}h (Urlaub bereits abgezogen, nominal ${employee.weekly_capacity_hours}h)`"
>
{{ percent(hours, employee.weekly_capacity_hours) }}%
{{ percent(hours, employee.capacity[idx]) }}%
</div>
</td>
</tr>
+146
View File
@@ -0,0 +1,146 @@
<script setup>
import { computed, reactive, ref } from 'vue';
import { store } from './store.js';
import { formatDate } from './helpers.js';
const role = computed(() => store.session.user?.role);
const isManager = computed(() => role.value === 'admin' || role.value === 'teamleiter');
const ownEmployeeId = computed(() => store.session.user?.employee_id);
const newVacation = reactive({
employee_id: ownEmployeeId.value || '',
start_date: '',
end_date: '',
substitute_employee_id: '',
note: '',
});
const createError = ref(null);
function substitutesFor(employeeId) {
return store.employees.filter((e) => e.id !== Number(employeeId));
}
async function addVacation() {
createError.value = null;
if (!newVacation.start_date || !newVacation.end_date || !newVacation.substitute_employee_id) return;
try {
await store.createVacation({ ...newVacation });
Object.assign(newVacation, {
employee_id: ownEmployeeId.value || '',
start_date: '',
end_date: '',
substitute_employee_id: '',
note: '',
});
} catch (e) {
createError.value = e.message;
}
}
function canEdit(vacation) {
return isManager.value || vacation.employee_id === ownEmployeeId.value;
}
const editingId = ref(null);
const editForm = reactive({ start_date: '', end_date: '', substitute_employee_id: '', note: '' });
const editError = ref(null);
function startEdit(vacation) {
editingId.value = vacation.id;
editError.value = null;
Object.assign(editForm, {
start_date: vacation.start_date,
end_date: vacation.end_date,
substitute_employee_id: vacation.substitute_employee_id,
note: vacation.note,
});
}
function cancelEdit() {
editingId.value = null;
}
async function saveEdit(id) {
editError.value = null;
try {
await store.updateVacation(id, { ...editForm });
editingId.value = null;
} catch (e) {
editError.value = e.message;
}
}
</script>
<template>
<div class="panel-section">
<h2>Neuer Urlaubseintrag</h2>
<div class="form-row">
<select v-if="isManager" v-model.number="newVacation.employee_id">
<option value="" disabled>Mitarbeiter wählen</option>
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ e.first_name }} {{ e.last_name }}</option>
</select>
<input v-model="newVacation.start_date" type="date" />
<input v-model="newVacation.end_date" type="date" />
<select v-model.number="newVacation.substitute_employee_id">
<option value="" disabled>Vertreter wählen</option>
<option v-for="e in substitutesFor(newVacation.employee_id)" :key="e.id" :value="e.id">
{{ e.first_name }} {{ e.last_name }}
</option>
</select>
<input v-model="newVacation.note" placeholder="Notiz (optional)" />
<button class="btn" @click="addVacation">Anlegen</button>
</div>
<p v-if="createError" class="muted" style="color: #ef4444">{{ createError }}</p>
</div>
<div class="panel-section">
<h2>Urlaubsplanung</h2>
<table>
<thead>
<tr>
<th>Mitarbeiter</th>
<th>Zeitraum</th>
<th>Vertreter</th>
<th>Notiz</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="v in store.vacations" :key="v.id">
<template v-if="editingId === v.id">
<td>{{ v.employee_name }}</td>
<td>
<input v-model="editForm.start_date" type="date" style="width: 130px" />
<input v-model="editForm.end_date" type="date" style="width: 130px" />
</td>
<td>
<select v-model.number="editForm.substitute_employee_id">
<option v-for="e in substitutesFor(v.employee_id)" :key="e.id" :value="e.id">
{{ e.first_name }} {{ e.last_name }}
</option>
</select>
</td>
<td><input v-model="editForm.note" style="width: 100%" /></td>
<td>
<button class="btn" @click="saveEdit(v.id)">Speichern</button>
<button class="btn secondary" @click="cancelEdit">Abbrechen</button>
</td>
</template>
<template v-else>
<td>{{ v.employee_name }}</td>
<td>{{ formatDate(v.start_date) }} {{ formatDate(v.end_date) }}</td>
<td>{{ v.substitute_name }}</td>
<td>{{ v.note }}</td>
<td v-if="canEdit(v)">
<button class="btn secondary" @click="startEdit(v)">Bearbeiten</button>
<button class="btn danger" @click="store.deleteVacation(v.id)">Löschen</button>
</td>
<td v-else></td>
</template>
</tr>
</tbody>
</table>
<p v-if="editError" class="muted" style="color: #ef4444">{{ editError }}</p>
<p v-if="store.vacations.length === 0" class="muted">Noch keine Urlaubseinträge vorhanden.</p>
</div>
</template>
+37 -2
View File
@@ -1,18 +1,53 @@
let authToken = localStorage.getItem('authToken') || null;
export function setAuthToken(token) {
authToken = token;
if (token) localStorage.setItem('authToken', token);
else localStorage.removeItem('authToken');
}
async function request(method, url, body) {
const headers = {};
if (body) headers['Content-Type'] = 'application/json';
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
const res = await fetch(url, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`${method} ${url} fehlgeschlagen: ${res.status} ${text}`);
let error;
try {
error = JSON.parse(text).error;
} catch {
error = text;
}
throw new Error(error || `${method} ${url} fehlgeschlagen: ${res.status}`);
}
const text = await res.text();
return text ? JSON.parse(text) : null;
}
export const api = {
// auth
login: (email, password) => request('POST', '/api/auth/login', { email, password }),
logout: () => request('POST', '/api/auth/logout'),
me: () => request('GET', '/api/auth/me'),
// vacations
getVacations: () => request('GET', '/api/vacations'),
createVacation: (data) => request('POST', '/api/vacations', data),
updateVacation: (id, data) => request('PUT', `/api/vacations/${id}`, data),
deleteVacation: (id) => request('DELETE', `/api/vacations/${id}`),
// users
getUsers: () => request('GET', '/api/users'),
createUser: (data) => request('POST', '/api/users', data),
updateUser: (id, data) => request('PUT', `/api/users/${id}`, data),
deleteUser: (id) => request('DELETE', `/api/users/${id}`),
// employees
getEmployees: () => request('GET', '/api/employees'),
createEmployee: (data) => request('POST', '/api/employees', data),
+84 -2
View File
@@ -1,27 +1,78 @@
import { reactive } from 'vue';
import { api } from './api.js';
import { api, setAuthToken } from './api.js';
import { fullName } from './helpers.js';
const storedUser = localStorage.getItem('authUser');
export const store = reactive({
employees: [],
teams: [],
projects: [],
vacations: [],
users: [],
utilization: null,
loading: false,
error: null,
session: {
token: localStorage.getItem('authToken') || null,
user: storedUser ? JSON.parse(storedUser) : null,
},
async restoreSession() {
if (!this.session.token) return;
try {
this.session.user = await api.me();
localStorage.setItem('authUser', JSON.stringify(this.session.user));
} catch {
this.logout();
}
},
async login(email, password) {
const { token, user } = await api.login(email, password);
setAuthToken(token);
localStorage.setItem('authUser', JSON.stringify(user));
this.session.token = token;
this.session.user = user;
await this.loadAll();
},
async logout() {
try {
if (this.session.token) await api.logout();
} catch {
// Sitzung ggf. schon abgelaufen, trotzdem lokal ausloggen
}
setAuthToken(null);
localStorage.removeItem('authUser');
this.session.token = null;
this.session.user = null;
this.employees = [];
this.teams = [];
this.projects = [];
this.vacations = [];
this.users = [];
this.utilization = null;
},
async loadAll() {
this.loading = true;
this.error = null;
try {
const [employees, teams, projects] = await Promise.all([
const [employees, teams, projects, vacations] = await Promise.all([
api.getEmployees(),
api.getTeams(),
api.getProjects(),
api.getVacations(),
]);
this.employees = employees;
this.teams = teams;
this.projects = projects;
this.vacations = vacations;
if (this.session.user?.role === 'admin') {
this.users = await api.getUsers();
}
await this.loadUtilization();
} catch (e) {
this.error = e.message;
@@ -142,4 +193,35 @@ export const store = reactive({
if (task) task.assignments = task.assignments.filter((a) => a.id !== assignmentId);
await this.loadUtilization();
},
// vacations
async createVacation(data) {
this.vacations.unshift(await api.createVacation(data));
await this.loadUtilization();
},
async updateVacation(id, data) {
const updated = await api.updateVacation(id, data);
const idx = this.vacations.findIndex((v) => v.id === id);
if (idx !== -1) this.vacations[idx] = updated;
await this.loadUtilization();
},
async deleteVacation(id) {
await api.deleteVacation(id);
this.vacations = this.vacations.filter((v) => v.id !== id);
await this.loadUtilization();
},
// users
async createUser(data) {
this.users.push(await api.createUser(data));
},
async updateUser(id, data) {
const updated = await api.updateUser(id, data);
const idx = this.users.findIndex((u) => u.id === id);
if (idx !== -1) this.users[idx] = updated;
},
async deleteUser(id) {
await api.deleteUser(id);
this.users = this.users.filter((u) => u.id !== id);
},
});