Vorname/Nachname für Mitarbeiter, Aufgaben-Duplizierung, Zeitstrahl im Zeitplan
- employees: name-Feld in first_name/last_name aufgeteilt (Schema, API, Formular, alle Namensanzeigen) - Aufgaben können mit neuem Start-/Enddatum dupliziert werden (POST /api/tasks/:id/duplicate übernimmt Name, Stunden, Status und Zuweisungen 1:1, nur die Termine ändern sich) - Zeitplan zeigt jetzt einen Zeitstrahl mit Kalenderwochen und Tagesnummern oberhalb der Projekt-Balken
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
import { reactive } from 'vue';
|
||||
import { store } from './store.js';
|
||||
|
||||
const newEmployee = reactive({ name: '', email: '', role: '', weekly_capacity_hours: 40 });
|
||||
const newEmployee = reactive({ first_name: '', last_name: '', email: '', role: '', weekly_capacity_hours: 40 });
|
||||
|
||||
async function addEmployee() {
|
||||
if (!newEmployee.name) return;
|
||||
if (!newEmployee.first_name || !newEmployee.last_name) return;
|
||||
await store.createEmployee({ ...newEmployee });
|
||||
Object.assign(newEmployee, { name: '', email: '', role: '', weekly_capacity_hours: 40 });
|
||||
Object.assign(newEmployee, { first_name: '', last_name: '', email: '', role: '', weekly_capacity_hours: 40 });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,8 @@ async function addEmployee() {
|
||||
<div class="panel-section">
|
||||
<h2>Neuer Mitarbeiter</h2>
|
||||
<div class="form-row">
|
||||
<input v-model="newEmployee.name" placeholder="Name" />
|
||||
<input v-model="newEmployee.first_name" placeholder="Vorname" />
|
||||
<input v-model="newEmployee.last_name" placeholder="Nachname" />
|
||||
<input v-model="newEmployee.email" placeholder="E-Mail" />
|
||||
<input v-model="newEmployee.role" placeholder="Rolle" />
|
||||
<input v-model.number="newEmployee.weekly_capacity_hours" type="number" style="width: 100px" placeholder="Std/Woche" />
|
||||
@@ -28,7 +29,8 @@ async function addEmployee() {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Vorname</th>
|
||||
<th>Nachname</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Rolle</th>
|
||||
<th>Kapazität (Std/Woche)</th>
|
||||
@@ -37,7 +39,8 @@ async function addEmployee() {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in store.employees" :key="e.id">
|
||||
<td>{{ e.name }}</td>
|
||||
<td>{{ e.first_name }}</td>
|
||||
<td>{{ e.last_name }}</td>
|
||||
<td>{{ e.email }}</td>
|
||||
<td>{{ e.role }}</td>
|
||||
<td>{{ e.weekly_capacity_hours }}</td>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { store } from './store.js';
|
||||
import { formatDate } from './helpers.js';
|
||||
import { formatDate, fullName } from './helpers.js';
|
||||
|
||||
const newProject = reactive({ name: '', description: '', start_date: '', end_date: '', color: '#4f46e5' });
|
||||
const newTask = reactive({});
|
||||
const newAssignment = reactive({});
|
||||
const newDuplicate = reactive({});
|
||||
const expanded = reactive({});
|
||||
|
||||
async function addProject() {
|
||||
@@ -37,6 +38,18 @@ async function addAssignment(projectId, taskId) {
|
||||
await store.assignEmployee(projectId, taskId, Number(form.employee_id), Number(form.allocated_hours_per_week));
|
||||
Object.assign(form, { employee_id: '', allocated_hours_per_week: 8 });
|
||||
}
|
||||
|
||||
function duplicateForm(taskId) {
|
||||
if (!newDuplicate[taskId]) newDuplicate[taskId] = { start_date: '', end_date: '' };
|
||||
return newDuplicate[taskId];
|
||||
}
|
||||
|
||||
async function duplicateTask(projectId, taskId) {
|
||||
const form = duplicateForm(taskId);
|
||||
if (!form.start_date || !form.end_date) return;
|
||||
await store.duplicateTask(projectId, taskId, { ...form });
|
||||
Object.assign(form, { start_date: '', end_date: '' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -103,15 +116,23 @@ async function addAssignment(projectId, taskId) {
|
||||
<button class="btn" @click="addTask(project.id)">Hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div v-for="task in project.tasks" :key="'assign-' + task.id" class="form-row">
|
||||
<span style="width: 180px">{{ task.name }}:</span>
|
||||
<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">{{ e.name }}</option>
|
||||
</select>
|
||||
<input v-model.number="assignmentForm(task.id).allocated_hours_per_week" type="number" style="width: 90px" placeholder="Std/Woche" />
|
||||
<button class="btn secondary" @click="addAssignment(project.id, task.id)">Zuweisen</button>
|
||||
<button class="btn danger" @click="store.deleteTask(project.id, task.id)">Aufgabe löschen</button>
|
||||
<div v-for="task in project.tasks" :key="'manage-' + task.id" style="border: 1px solid var(--border); border-radius: 6px; padding: 8px; margin-bottom: 8px">
|
||||
<strong>{{ task.name }}</strong>
|
||||
<div class="form-row">
|
||||
<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>
|
||||
</select>
|
||||
<input v-model.number="assignmentForm(task.id).allocated_hours_per_week" type="number" style="width: 90px" placeholder="Std/Woche" />
|
||||
<button class="btn secondary" @click="addAssignment(project.id, task.id)">Zuweisen</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span class="muted">Aufgabe zu anderem Zeitraum wiederholen:</span>
|
||||
<input v-model="duplicateForm(task.id).start_date" type="date" />
|
||||
<input v-model="duplicateForm(task.id).end_date" type="date" />
|
||||
<button class="btn secondary" @click="duplicateTask(project.id, task.id)">Duplizieren</button>
|
||||
<button class="btn danger" @click="store.deleteTask(project.id, task.id)">Aufgabe löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { store } from './store.js';
|
||||
import { ganttRange, dayOffset, daysBetween, formatDate, statusColor } from './helpers.js';
|
||||
import { ganttRange, dayOffset, daysBetween, formatDate, statusColor, buildTimeline } from './helpers.js';
|
||||
|
||||
const range = computed(() => ganttRange(store.projects));
|
||||
const totalDays = computed(() => Math.max(1, dayOffset(range.value.min, range.value.max.toISOString()) + 1));
|
||||
const timeline = computed(() => buildTimeline(range.value.min, range.value.max));
|
||||
|
||||
function barStyle(task) {
|
||||
const left = (dayOffset(range.value.min, task.start_date) / totalDays.value) * 100;
|
||||
@@ -15,6 +16,10 @@ function barStyle(task) {
|
||||
background: statusColor(task.status),
|
||||
};
|
||||
}
|
||||
|
||||
function rangeStyle(entry) {
|
||||
return { left: `${entry.leftPercent}%`, width: `${entry.widthPercent}%` };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,6 +29,30 @@ function barStyle(task) {
|
||||
{{ formatDate(range.min.toISOString()) }} – {{ formatDate(range.max.toISOString()) }}
|
||||
</p>
|
||||
|
||||
<div class="gantt-row">
|
||||
<div class="gantt-label"></div>
|
||||
<div class="timeline-track">
|
||||
<div v-for="(week, idx) in timeline.weeks" :key="'w' + idx" class="timeline-week" :style="rangeStyle(week)">
|
||||
KW {{ week.week }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gantt-row" style="margin-bottom: 10px">
|
||||
<div class="gantt-label"></div>
|
||||
<div class="timeline-track">
|
||||
<div
|
||||
v-for="(day, idx) in timeline.days"
|
||||
:key="'d' + idx"
|
||||
class="timeline-day"
|
||||
:class="{ weekend: day.isWeekend }"
|
||||
:style="rangeStyle(day)"
|
||||
:title="`${day.weekdayLabel}, ${day.date.toLocaleDateString('de-DE')}`"
|
||||
>
|
||||
{{ day.dayOfMonth }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="project in store.projects" :key="project.id" style="margin-bottom: 20px">
|
||||
<strong :style="{ color: project.color }">{{ project.name }}</strong>
|
||||
<div v-for="task in project.tasks" :key="task.id" class="gantt-row">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { store } from './store.js';
|
||||
import { fullName } from './helpers.js';
|
||||
|
||||
const newTeam = reactive({ name: '', description: '' });
|
||||
const newMember = reactive({});
|
||||
@@ -39,7 +40,7 @@ async function addMember(teamId) {
|
||||
<p class="muted">{{ team.description }}</p>
|
||||
|
||||
<span v-for="m in team.members" :key="m.id" class="tag">
|
||||
{{ m.name }}
|
||||
{{ fullName(m) }}
|
||||
<a 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>
|
||||
@@ -47,7 +48,7 @@ async function addMember(teamId) {
|
||||
<div 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">{{ e.name }}</option>
|
||||
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ fullName(e) }}</option>
|
||||
</select>
|
||||
<button class="btn secondary" @click="addMember(team.id)">Hinzufügen</button>
|
||||
<button class="btn danger" @click="store.deleteTeam(team.id)">Team löschen</button>
|
||||
|
||||
@@ -38,6 +38,7 @@ export const api = {
|
||||
createTask: (data) => request('POST', '/api/tasks', data),
|
||||
updateTask: (id, data) => request('PUT', `/api/tasks/${id}`, data),
|
||||
deleteTask: (id) => request('DELETE', `/api/tasks/${id}`),
|
||||
duplicateTask: (id, data) => request('POST', `/api/tasks/${id}/duplicate`, data),
|
||||
addDependency: (taskId, dependsOnId) =>
|
||||
request('POST', `/api/tasks/${taskId}/dependencies`, { depends_on_task_id: dependsOnId }),
|
||||
removeDependency: (taskId, dependsOnId) =>
|
||||
|
||||
@@ -37,6 +37,51 @@ export function utilizationColor(percent) {
|
||||
return '#f87171';
|
||||
}
|
||||
|
||||
export function fullName(person) {
|
||||
if (!person) return '';
|
||||
return `${person.first_name} ${person.last_name}`.trim();
|
||||
}
|
||||
|
||||
function isoWeekNumber(date) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = (d.getUTCDay() + 6) % 7;
|
||||
d.setUTCDate(d.getUTCDate() - dayNum + 3);
|
||||
const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
|
||||
return 1 + Math.round(((d - firstThursday) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7);
|
||||
}
|
||||
|
||||
const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
|
||||
export function buildTimeline(min, max) {
|
||||
const totalDays = Math.max(1, Math.round((max - min) / 86400000) + 1);
|
||||
const days = [];
|
||||
for (let i = 0; i < totalDays; i++) {
|
||||
const date = new Date(min.getTime() + i * 86400000);
|
||||
const weekday = (date.getDay() + 6) % 7; // Montag = 0
|
||||
days.push({
|
||||
date,
|
||||
leftPercent: (i / totalDays) * 100,
|
||||
widthPercent: (1 / totalDays) * 100,
|
||||
dayOfMonth: date.getDate(),
|
||||
weekdayLabel: WEEKDAY_LABELS[weekday],
|
||||
isWeekend: weekday >= 5,
|
||||
week: isoWeekNumber(date),
|
||||
});
|
||||
}
|
||||
|
||||
const weeks = [];
|
||||
for (const day of days) {
|
||||
const last = weeks[weeks.length - 1];
|
||||
if (last && last.week === day.week) {
|
||||
last.widthPercent += day.widthPercent;
|
||||
} else {
|
||||
weeks.push({ week: day.week, leftPercent: day.leftPercent, widthPercent: day.widthPercent });
|
||||
}
|
||||
}
|
||||
|
||||
return { days, weeks };
|
||||
}
|
||||
|
||||
export function statusColor(status) {
|
||||
switch (status) {
|
||||
case 'erledigt':
|
||||
|
||||
@@ -163,6 +163,43 @@ button.btn.danger {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-track {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.timeline-week {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid var(--border);
|
||||
padding-left: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-day {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid #f1f5f9;
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.timeline-day.weekend {
|
||||
background: #f8fafc;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.heatmap-table td, .heatmap-table th {
|
||||
text-align: center;
|
||||
min-width: 56px;
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
import { reactive } from 'vue';
|
||||
import { api } from './api.js';
|
||||
import { fullName } from './helpers.js';
|
||||
|
||||
export const store = reactive({
|
||||
employees: [],
|
||||
@@ -110,6 +111,12 @@ export const store = reactive({
|
||||
if (project) project.tasks = project.tasks.filter((t) => t.id !== taskId);
|
||||
await this.loadUtilization();
|
||||
},
|
||||
async duplicateTask(projectId, taskId, dates) {
|
||||
const newTask = await api.duplicateTask(taskId, dates);
|
||||
const project = this.projects.find((p) => p.id === projectId);
|
||||
if (project) project.tasks.push(newTask);
|
||||
await this.loadUtilization();
|
||||
},
|
||||
|
||||
// assignments
|
||||
async assignEmployee(projectId, taskId, employeeId, hoursPerWeek) {
|
||||
@@ -123,8 +130,8 @@ export const store = reactive({
|
||||
if (task) {
|
||||
const employee = this.employees.find((e) => e.id === employeeId);
|
||||
const existing = task.assignments.find((a) => a.employee_id === employeeId);
|
||||
if (existing) Object.assign(existing, assignment, { employee_name: employee?.name });
|
||||
else task.assignments.push({ ...assignment, employee_name: employee?.name });
|
||||
if (existing) Object.assign(existing, assignment, { employee_name: fullName(employee) });
|
||||
else task.assignments.push({ ...assignment, employee_name: fullName(employee) });
|
||||
}
|
||||
await this.loadUtilization();
|
||||
},
|
||||
|
||||
+6
-6
@@ -20,14 +20,14 @@ if (isNew) {
|
||||
|
||||
function seed() {
|
||||
const insertEmployee = db.prepare(
|
||||
'INSERT INTO employees (name, email, role, weekly_capacity_hours) VALUES (?, ?, ?, ?)'
|
||||
'INSERT INTO employees (first_name, last_name, email, role, weekly_capacity_hours) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
const employees = [
|
||||
['Anna Keller', 'anna.keller@example.com', 'Frontend-Entwicklerin', 40],
|
||||
['Jonas Weber', 'jonas.weber@example.com', 'Backend-Entwickler', 40],
|
||||
['Mira Schulz', 'mira.schulz@example.com', 'UX-Designerin', 32],
|
||||
['Tom Fischer', 'tom.fischer@example.com', 'Projektleiter', 40],
|
||||
['Lea Hoffmann', 'lea.hoffmann@example.com', 'QA-Ingenieurin', 30],
|
||||
['Anna', 'Keller', 'anna.keller@example.com', 'Frontend-Entwicklerin', 40],
|
||||
['Jonas', 'Weber', 'jonas.weber@example.com', 'Backend-Entwickler', 40],
|
||||
['Mira', 'Schulz', 'mira.schulz@example.com', 'UX-Designerin', 32],
|
||||
['Tom', 'Fischer', 'tom.fischer@example.com', 'Projektleiter', 40],
|
||||
['Lea', 'Hoffmann', 'lea.hoffmann@example.com', 'QA-Ingenieurin', 30],
|
||||
];
|
||||
const employeeIds = employees.map((e) => insertEmployee.run(...e).lastInsertRowid);
|
||||
|
||||
|
||||
+46
-11
@@ -41,28 +41,31 @@ function parseDate(s) {
|
||||
// ---------- employees ----------
|
||||
|
||||
app.get('/api/employees', (req, res) => {
|
||||
res.json(db.prepare('SELECT * FROM employees ORDER BY name').all());
|
||||
res.json(db.prepare('SELECT * FROM employees ORDER BY last_name, first_name').all());
|
||||
});
|
||||
|
||||
app.post('/api/employees', (req, res) => {
|
||||
const { name, email, role, weekly_capacity_hours } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'name ist erforderlich' });
|
||||
const { first_name, last_name, email, role, weekly_capacity_hours } = req.body;
|
||||
if (!first_name || !last_name) {
|
||||
return res.status(400).json({ error: 'first_name und last_name sind erforderlich' });
|
||||
}
|
||||
const result = db
|
||||
.prepare(
|
||||
'INSERT INTO employees (name, email, role, weekly_capacity_hours) VALUES (?, ?, ?, ?)'
|
||||
'INSERT INTO employees (first_name, last_name, email, role, weekly_capacity_hours) VALUES (?, ?, ?, ?, ?)'
|
||||
)
|
||||
.run(name, email || null, role || null, weekly_capacity_hours ?? 40);
|
||||
.run(first_name, last_name, email || null, role || null, weekly_capacity_hours ?? 40);
|
||||
res.status(201).json(db.prepare('SELECT * FROM employees WHERE id = ?').get(result.lastInsertRowid));
|
||||
});
|
||||
|
||||
app.put('/api/employees/:id', (req, res) => {
|
||||
const { name, email, role, weekly_capacity_hours } = req.body;
|
||||
const { first_name, last_name, email, role, weekly_capacity_hours } = req.body;
|
||||
const existing = db.prepare('SELECT * FROM employees WHERE id = ?').get(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'nicht gefunden' });
|
||||
db.prepare(
|
||||
'UPDATE employees SET name = ?, email = ?, role = ?, weekly_capacity_hours = ? WHERE id = ?'
|
||||
'UPDATE employees SET first_name = ?, last_name = ?, email = ?, role = ?, weekly_capacity_hours = ? WHERE id = ?'
|
||||
).run(
|
||||
name ?? existing.name,
|
||||
first_name ?? existing.first_name,
|
||||
last_name ?? existing.last_name,
|
||||
email ?? existing.email,
|
||||
role ?? existing.role,
|
||||
weekly_capacity_hours ?? existing.weekly_capacity_hours,
|
||||
@@ -144,7 +147,7 @@ app.get('/api/projects', (req, res) => {
|
||||
const tasks = db.prepare('SELECT * FROM tasks ORDER BY start_date').all();
|
||||
const assignments = db
|
||||
.prepare(
|
||||
`SELECT a.*, e.name AS employee_name FROM assignments a
|
||||
`SELECT a.*, (e.first_name || ' ' || e.last_name) AS employee_name FROM assignments a
|
||||
JOIN employees e ON e.id = a.employee_id`
|
||||
)
|
||||
.all();
|
||||
@@ -231,6 +234,38 @@ app.delete('/api/tasks/:id', (req, res) => {
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
app.post('/api/tasks/:id/duplicate', (req, res) => {
|
||||
const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'nicht gefunden' });
|
||||
const { start_date, end_date } = req.body;
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ error: 'start_date und end_date sind erforderlich' });
|
||||
}
|
||||
const result = db
|
||||
.prepare(
|
||||
'INSERT INTO tasks (project_id, name, start_date, end_date, estimated_hours, status) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.run(existing.project_id, existing.name, start_date, end_date, existing.estimated_hours, existing.status);
|
||||
const newTaskId = result.lastInsertRowid;
|
||||
|
||||
const insertAssignment = db.prepare(
|
||||
'INSERT INTO assignments (task_id, employee_id, allocated_hours_per_week) VALUES (?, ?, ?)'
|
||||
);
|
||||
const originalAssignments = db.prepare('SELECT * FROM assignments WHERE task_id = ?').all(req.params.id);
|
||||
for (const a of originalAssignments) {
|
||||
insertAssignment.run(newTaskId, a.employee_id, a.allocated_hours_per_week);
|
||||
}
|
||||
|
||||
const newTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(newTaskId);
|
||||
const newAssignments = db
|
||||
.prepare(
|
||||
`SELECT a.*, (e.first_name || ' ' || e.last_name) AS employee_name FROM assignments a
|
||||
JOIN employees e ON e.id = a.employee_id WHERE a.task_id = ?`
|
||||
)
|
||||
.all(newTaskId);
|
||||
res.status(201).json({ ...newTask, assignments: newAssignments, depends_on: [] });
|
||||
});
|
||||
|
||||
app.post('/api/tasks/:id/dependencies', (req, res) => {
|
||||
const { depends_on_task_id } = req.body;
|
||||
db.prepare(
|
||||
@@ -291,7 +326,7 @@ app.get('/api/utilization', (req, res) => {
|
||||
weeks.push({ key: isoWeekKey(weekStart), start: weekStart });
|
||||
}
|
||||
|
||||
const employees = db.prepare('SELECT * FROM employees ORDER BY name').all();
|
||||
const employees = db.prepare('SELECT * FROM employees ORDER BY last_name, first_name').all();
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT a.employee_id, a.allocated_hours_per_week, t.start_date, t.end_date
|
||||
@@ -320,7 +355,7 @@ app.get('/api/utilization', (req, res) => {
|
||||
weeks: weeks.map((w) => w.key),
|
||||
employees: employees.map((e) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
name: `${e.first_name} ${e.last_name}`,
|
||||
weekly_capacity_hours: e.weekly_capacity_hours,
|
||||
hours: weeks.map((w) => Math.round((data[e.id][w.key] || 0) * 10) / 10),
|
||||
})),
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@ PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employees (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
role TEXT,
|
||||
weekly_capacity_hours REAL NOT NULL DEFAULT 40
|
||||
|
||||
Reference in New Issue
Block a user