Files
tilbudgivern/backend/src/services/planningService.js
T

487 lines
15 KiB
JavaScript

const logger = require('../utils/logger');
/**
* PlanningService - Håndterer planlægning og allokering af medarbejdere til projekter
* Integrerer med Ordrestyring API data for aktuelle ordrer og medarbejderinformation
*/
class PlanningService {
constructor(databaseService) {
this.db = databaseService;
this.ORDRESTYRING_DB = 'ordrestyring_local';
}
/**
* Hent alle igangværende ordrer fra Ordrestyring
* @returns {Array} Active orders with details
*/
async getActiveOrders() {
try {
const query = `
SELECT
c.id,
c.case_number,
c.yourref,
c.description,
c.creation_date,
c.status,
c.main_technician,
c.additional_technicians,
c.contact,
c.delivery_address,
c.remarks,
cs.text as status_text,
cs.colorcode as status_color,
ct.text as case_type_text,
ct.colorcode as case_type_color,
u.first_name as main_tech_first_name,
u.last_name as main_tech_last_name,
u.init as main_tech_init,
d.customer_name,
d.customer_address,
d.customer_telephone,
d.customer_email
FROM ${this.ORDRESTYRING_DB}.cases c
LEFT JOIN ${this.ORDRESTYRING_DB}.case_statuses cs ON c.status = cs.id
LEFT JOIN ${this.ORDRESTYRING_DB}.case_types ct ON c.case_type = ct.id
LEFT JOIN ${this.ORDRESTYRING_DB}.users u ON c.main_technician = u.id
LEFT JOIN ${this.ORDRESTYRING_DB}.debtors d ON c.customer_number = d.customer_number
WHERE c.status IN (99991, 999963, 999966) -- Active statuses (Åben, Åben/VIGTIG, Åben - Tag)
ORDER BY c.creation_date DESC
LIMIT 50
`;
const orders = await this.db.query(query);
// Enrich with additional technicians and hours
for (let order of orders) {
// Parse additional technicians
if (order.additional_technicians) {
try {
const techIds = JSON.parse(order.additional_technicians);
if (Array.isArray(techIds) && techIds.length > 0) {
const techQuery = `
SELECT id, first_name, last_name, init
FROM ${this.ORDRESTYRING_DB}.users
WHERE id IN (${techIds.map(() => '?').join(',')})
`;
order.additional_technician_details = await this.db.query(techQuery, techIds);
}
} catch (e) {
console.warn('Failed to parse additional_technicians:', e);
}
}
// Get total hours for this case
const hoursQuery = `
SELECT
COUNT(*) as total_sessions,
SUM(CASE WHEN stop_time > 0 THEN (stop_time - start_time) / 3600 ELSE 0 END) as total_hours,
MAX(start_time) as last_activity
FROM ${this.ORDRESTYRING_DB}.hours
WHERE new_case_number = ? OR new_case_number = ?
`;
const hoursResult = await this.db.query(hoursQuery, [order.case_number, order.id]);
order.hours_summary = hoursResult[0] || { total_sessions: 0, total_hours: 0, last_activity: null };
// Get materials count
const materialsQuery = `
SELECT COUNT(*) as materials_count, SUM(CAST(sales_price as DECIMAL(10,2))) as total_value
FROM ${this.ORDRESTYRING_DB}.case_materials
WHERE case_number = ?
`;
const materialsResult = await this.db.query(materialsQuery, [order.case_number]);
order.materials_summary = materialsResult[0] || { materials_count: 0, total_value: 0 };
}
logger.info('Retrieved active orders', { count: orders.length });
return orders;
} catch (error) {
logger.error('Error fetching active orders', error);
throw error;
}
}
/**
* Hent alle medarbejdere fra Ordrestyring
* @returns {Array} All employees with their details
*/
async getEmployees() {
try {
const query = `
SELECT
u.id,
u.init,
u.first_name,
u.last_name,
u.fullName,
u.email,
u.tlf,
u.mobil_privat,
u.status,
u.department_id,
u.employee_type_id,
et.title as employee_type_title,
et.color as employee_type_color,
et.is_personal
FROM ${this.ORDRESTYRING_DB}.users u
LEFT JOIN ${this.ORDRESTYRING_DB}.employee_types et ON u.employee_type_id = et.id
WHERE u.status IN (999912, 999913) -- Active employees
ORDER BY u.first_name, u.last_name
`;
const employees = await this.db.query(query);
// Get current week allocations for each employee
const startOfWeek = this.getStartOfWeek(new Date());
const endOfFourWeeks = new Date(startOfWeek);
endOfFourWeeks.setDate(endOfFourWeeks.getDate() + 28); // 4 weeks
for (let employee of employees) {
// Get current allocations
const allocationsQuery = `
SELECT
h.id,
h.start_time,
h.stop_time,
h.remark,
h.new_case_number,
c.description as case_description,
c.yourref
FROM ${this.ORDRESTYRING_DB}.hours h
LEFT JOIN ${this.ORDRESTYRING_DB}.cases c ON h.new_case_number = c.case_number
WHERE h.emp_id = ?
AND h.start_time >= ?
AND h.start_time <= ?
ORDER BY h.start_time
`;
employee.upcoming_allocations = await this.db.query(allocationsQuery, [
employee.id,
Math.floor(startOfWeek.getTime() / 1000),
Math.floor(endOfFourWeeks.getTime() / 1000)
]);
// Calculate weekly workload
employee.weekly_hours = this.calculateWeeklyHours(employee.upcoming_allocations);
}
logger.info('Retrieved employees with allocations', { count: employees.length });
return employees;
} catch (error) {
logger.error('Error fetching employees', error);
throw error;
}
}
/**
* Hent kalender data for de næste 4 uger
* @returns {Object} Calendar data structured by week and employee
*/
async getCalendarData() {
try {
const startDate = this.getStartOfWeek(new Date());
const fourWeeksData = {};
// Generate 4 weeks of data
for (let week = 0; week < 4; week++) {
const weekStart = new Date(startDate);
weekStart.setDate(weekStart.getDate() + (week * 7));
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const weekKey = `week_${week + 1}`;
fourWeeksData[weekKey] = {
start_date: weekStart,
end_date: weekEnd,
week_number: this.getWeekNumber(weekStart),
employees: {}
};
// Get all allocations for this week
const allocationsQuery = `
SELECT
h.id,
h.emp_id,
h.start_time,
h.stop_time,
h.remark,
h.new_case_number,
h.hour_type,
c.description as case_description,
c.yourref,
c.status as case_status,
cs.text as case_status_text,
cs.colorcode as case_status_color,
u.first_name,
u.last_name,
u.init
FROM ${this.ORDRESTYRING_DB}.hours h
LEFT JOIN ${this.ORDRESTYRING_DB}.cases c ON h.new_case_number = c.case_number
LEFT JOIN ${this.ORDRESTYRING_DB}.case_statuses cs ON c.status = cs.id
LEFT JOIN ${this.ORDRESTYRING_DB}.users u ON h.emp_id = u.id
WHERE h.start_time >= ? AND h.start_time <= ?
ORDER BY h.emp_id, h.start_time
`;
const weekAllocations = await this.db.query(allocationsQuery, [
Math.floor(weekStart.getTime() / 1000),
Math.floor(weekEnd.getTime() / 1000)
]);
// First get ALL active employees (include common status codes)
const allEmployeesQuery = `
SELECT id, first_name, last_name, init, employee_type_id, department_id
FROM ${this.ORDRESTYRING_DB}.users
WHERE status IN (1, 999912, 999913) AND employee_type_id IS NOT NULL
ORDER BY first_name, last_name
`;
const allEmployees = await this.db.query(allEmployeesQuery);
// Initialize all employees with empty allocations
allEmployees.forEach(employee => {
fourWeeksData[weekKey].employees[employee.id] = {
employee_info: {
id: employee.id,
name: `${employee.first_name} ${employee.last_name}`,
init: employee.init,
employee_type_id: employee.employee_type_id,
department_id: employee.department_id
},
allocations: [],
total_hours: 0,
status: 'ledig' // Default status
};
});
// Group allocations by employee
weekAllocations.forEach(allocation => {
const employeeId = allocation.emp_id;
if (fourWeeksData[weekKey].employees[employeeId]) {
const duration = allocation.stop_time > 0 ?
(allocation.stop_time - allocation.start_time) / 3600 : 0;
fourWeeksData[weekKey].employees[employeeId].allocations.push({
...allocation,
duration_hours: duration,
start_date: new Date(allocation.start_time * 1000),
end_date: allocation.stop_time > 0 ? new Date(allocation.stop_time * 1000) : null
});
fourWeeksData[weekKey].employees[employeeId].total_hours += duration;
fourWeeksData[weekKey].employees[employeeId].status = 'allokeret';
}
});
}
return fourWeeksData;
} catch (error) {
logger.error('Error fetching calendar data', error);
throw error;
}
}
/**
* Opret ny medarbejder allokering
* @param {Object} allocationData - Allocation details
* @returns {Object} Created allocation
*/
async createAllocation(allocationData) {
try {
const {
employee_id,
case_number,
start_time,
end_time,
remark,
hour_type = 1 // Default work type
} = allocationData;
// Validate employee exists
const employeeCheck = await this.db.query(
`SELECT id FROM ${this.ORDRESTYRING_DB}.users WHERE id = ? AND status = 1`,
[employee_id]
);
if (employeeCheck.length === 0) {
throw new Error('Employee not found or inactive');
}
// Validate case exists
if (case_number) {
const caseCheck = await this.db.query(
`SELECT case_number FROM ${this.ORDRESTYRING_DB}.cases WHERE case_number = ?`,
[case_number]
);
if (caseCheck.length === 0) {
throw new Error('Case not found');
}
}
// Create allocation entry
const insertQuery = `
INSERT INTO ${this.ORDRESTYRING_DB}.hours (
emp_id,
start_time,
stop_time,
remark,
hour_type,
new_case_number,
exported,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, -1, ?, ?)
`;
const now = Math.floor(Date.now() / 1000);
const result = await this.db.query(insertQuery, [
employee_id,
Math.floor(new Date(start_time).getTime() / 1000),
end_time ? Math.floor(new Date(end_time).getTime() / 1000) : 0,
remark || '',
hour_type,
case_number || null,
now,
now
]);
logger.info('Created new allocation', {
id: result.insertId,
employee_id,
case_number,
start_time,
end_time
});
return {
success: true,
allocation_id: result.insertId,
message: 'Allokering oprettet succesfuldt'
};
} catch (error) {
logger.error('Error creating allocation', error);
throw error;
}
}
/**
* Opdater eksisterende allokering
* @param {number} allocationId - ID of allocation to update
* @param {Object} updateData - Updated allocation data
* @returns {Object} Update result
*/
async updateAllocation(allocationId, updateData) {
try {
const {
start_time,
end_time,
remark,
case_number
} = updateData;
const updateQuery = `
UPDATE ${this.ORDRESTYRING_DB}.hours
SET
start_time = COALESCE(?, start_time),
stop_time = COALESCE(?, stop_time),
remark = COALESCE(?, remark),
new_case_number = COALESCE(?, new_case_number),
updated_at = ?
WHERE id = ?
`;
const now = Math.floor(Date.now() / 1000);
await this.db.query(updateQuery, [
start_time ? Math.floor(new Date(start_time).getTime() / 1000) : null,
end_time ? Math.floor(new Date(end_time).getTime() / 1000) : null,
remark || null,
case_number || null,
now,
allocationId
]);
logger.info('Updated allocation', { allocationId, updateData });
return {
success: true,
message: 'Allokering opdateret succesfuldt'
};
} catch (error) {
logger.error('Error updating allocation', error);
throw error;
}
}
/**
* Slet allokering
* @param {number} allocationId - ID of allocation to delete
* @returns {Object} Delete result
*/
async deleteAllocation(allocationId) {
try {
await this.db.query(
`DELETE FROM ${this.ORDRESTYRING_DB}.hours WHERE id = ?`,
[allocationId]
);
logger.info('Deleted allocation', { allocationId });
return {
success: true,
message: 'Allokering slettet succesfuldt'
};
} catch (error) {
logger.error('Error deleting allocation', error);
throw error;
}
}
/**
* Hjælpe funktioner
*/
getStartOfWeek(date) {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Mandag som start
return new Date(d.setDate(diff));
}
getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
calculateWeeklyHours(allocations) {
const weeklyHours = {};
allocations.forEach(allocation => {
const startDate = new Date(allocation.start_time * 1000);
const weekKey = this.getWeekNumber(startDate);
if (!weeklyHours[weekKey]) {
weeklyHours[weekKey] = 0;
}
if (allocation.stop_time > 0) {
const hours = (allocation.stop_time - allocation.start_time) / 3600;
weeklyHours[weekKey] += hours;
}
});
return weeklyHours;
}
}
module.exports = PlanningService;