This commit is contained in:
DarrenT~ 2024-10-03 13:55:18 +02:00
commit 7073d67489
24 changed files with 28047 additions and 0 deletions

6
.env.example Normal file

@ -0,0 +1,6 @@
# fleks planning production
PSQL_HOSTNAME=
PSQL_USERNAME=
PSQL_PASSWORD=
PSQL_DATABASE=
PSQL_PORT=5432

4
.gitignore vendored Normal file

@ -0,0 +1,4 @@
.vscode
.bkup
.env
node_modules

0
README.md Normal file

32
api/auth.js Normal file

@ -0,0 +1,32 @@
export const login = async (username, password) => {
const apiProdUrl = 'https://backend.fleks.works/api/users/login/'
let schema = {
username,
password,
"user_type": "dashboard",
"breakpoint": true,
"login_from": "dashboard"
}
try {
const response = await fetch(apiProdUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(schema)
});
const data = await response.json().then((data) => {
console.log({ data })
return data
});
const token = data?.results?.token
return token
} catch (err) {
console.log(err)
return err
}
}

84
api/jobs.js Normal file

@ -0,0 +1,84 @@
import express from 'express';
import fetch from 'node-fetch';
import { promises as fs } from 'fs';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { convertToAmsterdamTime } from './utils.js';
import { PauzeManager } from '../services/PauzeManager.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const router = express.Router();
export const getJobs = async (req, res) => {
const apiKey = req.body.apiKey;
const apiProdUrl = `https://api.fleks.works/v1/jobs/?limit=100000&page=1&start_date=2024-06-26&isArchived=false`;
try {
const timeTableData = await fs.readFile(path.join('data', 'colorcrew_data_transformed_prod.json'), 'utf8');
const timeTable = JSON.parse(timeTableData);
const response = await fetch(apiProdUrl, {
headers: {
'x-api-key': apiKey
}
});
const data = await response.json();
const pauzeManager = new PauzeManager();
const updatedJobs = await pauzeManager.updateJobsWithPauze(data.results, timeTable, convertToAmsterdamTime);
await pauzeManager.updateJobsInDatabase(37, updatedJobs)
res.json({ amountOfJobs: updatedJobs.length, updatedJobs: updatedJobs });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Error fetching data' });
}
};
export const processAndUpdateJobs = async (req, res) => {
const { tenantId, jobs } = req.body;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID is required' });
}
if (!jobs || !Array.isArray(jobs)) {
return res.status(400).json({ error: 'Error with fetching Jobs' });
}
try {
const timeTableData = await fs.readFile(path.join(__dirname, 'data', 'colorcrew_data_transformed_prod.json'), 'utf8');
const timeTable = JSON.parse(timeTableData);
const pauzeManager = new PauzeManager();
const updatedJobs = pauzeManager.updateJobsWithPauze(jobs, timeTable, convertToAmsterdamTime);
const result = await pauzeManager.updateJobsInDatabase(tenantId, updatedJobs);
res.json({ message: 'Jobs updated successfully.', result });
} catch (error) {
console.error('Error updating jobs:', error);
res.status(500).json({ error: 'Error updating jobs' });
}
};
export const removePauzeFromJobs = async (req, res) => {
const pauzeManager = new PauzeManager();
try {
await pauzeManager.removePauzeFromDatabase(8);
res.json({ message: 'Pauze removed from job descriptions successfully.' });
} catch (error) {
console.error('Error removing pauze from job descriptions:', error);
res.status(500).json({ error: 'Error removing pauze from job descriptions' });
}
};
export default router;

110
api/shifts.js Normal file

@ -0,0 +1,110 @@
import { login } from './auth.js'
import { convertToAmsterdamTime } from "./utils.js";
export const getShifts = async (req, res) => {
const apiKey = req.body.apiKey;
// const apiProdUrl = 'https://api.fleks.works/v1/shifts/?limit=100000&page=1&isArchived=false&is_approved=false&is_exported=false&is_invoiced=false&is_paid_out=true&noShiftComments=true';
const apiProdUrl = 'https://api.fleks.works/v1/shifts/?limit=100000&page=1&isArchived=false&is_approved=false&noShiftComments=true';
try {
const response = await fetch(apiProdUrl, {
headers: {
'x-api-key': apiKey
}
});
const data = await response.json();
const shifts = data.results.map(shift => {
const startDate = new Date(shift.start_date);
const endDate = new Date(shift.end_date);
const startAmsterdamTime = convertToAmsterdamTime(startDate);
const endAmsterdamTime = convertToAmsterdamTime(endDate);
return {
...shift,
start_time: startAmsterdamTime,
end_time: endAmsterdamTime
};
});
res.json({ ...data, results: shifts });
} catch (error) {
console.log(error)
res.status(500).json({ error: 'Error fetching data' });
}
}
export const updateShifts = async (req, res) => {
const { config, shifts } = req.body; // Expected type = { config , shifts }
if (!Array.isArray(shifts)) {
console.error('Request body should be an array of updates');
return res.status(400).json({ error: 'Request body should be an array of updates' });
}
const results = {
success: [],
failed: []
};
const getToken = async () => {
return await login(config?.userName, config?.passWord);
}
let token = await getToken();
if (!token) {
const error = `token not obtained`;
console.log(error);
return res.status(403).send({ error });
}
const updateShift = async (update) => {
const { uuid, break_hours } = update;
const apiProdUrl = `https://backend.fleks.works/api/jobs/workflow-fields/${uuid}/`;
try {
const response = await fetch(apiProdUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `JWT ${token}`
},
body: JSON.stringify({ break_hours })
});
const data = await response.json();
if (response.ok) {
console.log(`Successfully updated break hours for UUID: ${uuid}`);
results.success.push({ uuid, message: data.message });
} else {
if (response.status === 401 || response.status === 403) {
console.log(`Token invalid. Reattempting login for UUID: ${uuid}`);
token = await getToken();
if (token) {
return updateShift(update);
} else {
console.error(`Failed to update break hours for UUID: ${uuid}`, data);
results.failed.push({ uuid, error: 'Failed to reauthenticate and obtain new token' });
}
} else {
console.error(`Failed to update break hours for UUID: ${uuid}`, data);
results.failed.push({ uuid, error: data.message || data });
}
}
} catch (error) {
console.error(`Error updating break hours for UUID: ${uuid}`, error);
results.failed.push({ uuid, error: error.message });
}
}
for (const update of shifts) {
await updateShift(update);
}
console.log(`Finished processing updates. Success: ${results.success.length}, Failed: ${results.failed.length}`);
res.json(results);
}

7
api/utils.js Normal file

@ -0,0 +1,7 @@
import { utcToZonedTime, format } from 'date-fns-tz';
export function convertToAmsterdamTime(date) {
const timeZone = 'Europe/Amsterdam';
const zonedDate = utcToZonedTime(date, timeZone);
return format(zonedDate, 'HH:mm:ss', { timeZone });
}

21
certs/server.cert Normal file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUeRKUyCrB8DwgDrYFncZqCWfRnnYwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCbmwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA1MzAyMTIwMTlaFw0yNDA2
MjkyMTIwMTlaMEUxCzAJBgNVBAYTAm5sMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCgO95YntjwazFQCkeiJUdnwMAwVwfgih0ZSQ0JhfoT
L1IC3D9me5a7AO3cWEEWkYMbF2/dfcb6IhfZPD6+nzy9JMqnDTUCOPZqf64fd/tG
j/+Adm1vZ+LH9l6p6iQ48fve3huZV5iqZnecrNKZurPfEEJTZPP0IdCDTVppBVrk
Ayt84wpofE2t989rW8gKCWR1vG4BWb8eZ935SjaAXnWVfY+qK6zoVOjFPL7do2W6
NcH2S0+0rTZEXIbIvoGcWBJ5PdLHQjs2QX92nQPdjMIadqGDzxEeo6gP1wuTZQyw
/NB9GwclDyppoMPt4vaG6DrSaKjej6841I5nsNMhWxflAgMBAAGjUzBRMB0GA1Ud
DgQWBBTsmvBk+GsgdXYKW0sVK66xJkN3+DAfBgNVHSMEGDAWgBTsmvBk+GsgdXYK
W0sVK66xJkN3+DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBW
otOxOXsJ/BG8e1usaEzp0l3wB7lrUAEnufMjpQEE3MqboToIDd8xfY3PNXrSHcvy
3hPUHVjBg0P0neyK6xZhl940UJLya6plzWnAik2WDN27o5jkG/MEeirKyiOjZlZo
Z6l831EO4YTS4jutVcvXYaohF/nk4ERe1L5dGRIDwA9cS37CUWtAkx67eANrXWiF
A8BAG6SxGOwJPRbsmZMW9yL/GCrvLnmr98fLa611mN720UYNjQw+d4HMT3rJJLY1
uTJ/JUL6hLwWjoBYT7/ypRSlJFJhG9/iyak9AaeUJ6st98M/cIz4gc2uLyudaLCV
CdmTuMfiHGJogF9WeiWR
-----END CERTIFICATE-----

28
certs/server.key Normal file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCgO95YntjwazFQ
CkeiJUdnwMAwVwfgih0ZSQ0JhfoTL1IC3D9me5a7AO3cWEEWkYMbF2/dfcb6IhfZ
PD6+nzy9JMqnDTUCOPZqf64fd/tGj/+Adm1vZ+LH9l6p6iQ48fve3huZV5iqZnec
rNKZurPfEEJTZPP0IdCDTVppBVrkAyt84wpofE2t989rW8gKCWR1vG4BWb8eZ935
SjaAXnWVfY+qK6zoVOjFPL7do2W6NcH2S0+0rTZEXIbIvoGcWBJ5PdLHQjs2QX92
nQPdjMIadqGDzxEeo6gP1wuTZQyw/NB9GwclDyppoMPt4vaG6DrSaKjej6841I5n
sNMhWxflAgMBAAECggEBAJJ6iGWNORZ3d4oLC7cfyyn+2/KU7P+IYteFn2RwVM8K
+DbjLxY5ru5fCBLhnwbJmQfAIiRh4e8yEYkmeNl76mOiaZvTB/1zI1jyRbRA07WK
1/CQ0rQATSGtiJZeFCT2meEAPEyu9kH4ECprFs8wDVTCoU9pP1aTPvF5WkgdfBxp
kw0NyrROxO4Q+/LXbrE2PNQIKyK4JNeZvVm9YHpEl8QFOo/JpuYLWoykSScldgHr
xEaRar4U10o+XJK+kOT7KJCxLL3Ednmi2yZOS7288Bj+98IYUaci7hcg1Z/ZN45R
c0a5W/mzM8ptT+fhb1RczAzhEX79/H84RyAqiEWsmRUCgYEAzp5xIzRxzg4NF846
GZxeoWsoEgw0TZmtpTINVq9NQCfoM1IMyz8lP6O51jEp8gN0xme5N6bpDRmH59xO
lQEOHDeHRWEgUSs0PuBv4BjYux0SxZo0XJo9HskL/cRxgs30X8fqJm7C4f3/I6nT
HELMJkQGSOJyuOOIWIq/a1L6+jsCgYEAxodzlV3DzyeQU3a+23emKfj7ugy1790f
923dYm92g1Friug2yCBvpXklqgv17Io+L2Qs3GImveFdw4ADbPcfxbC7LINZtLCk
YX8iZsXxmzUzEz9m0sobm64cSOn5d8NDXeJrAB9pw7g2Mm6FdSaJIli4Fv47AO8C
73jRxCuo9F8CgYBvjvq1MGbWA54sIUwbceNiMlJDVFWVJImuLRUonaQPJLzpoL6J
qsF41/TJ4mesZRNS4MQPeU5RpVxM4xWGvDgbIhwmaKejS7l8zX96NtAmTy9Ig9cL
vLeNfK29yagkIQF2CaGyOJF+pb5xSgtTMfm6G3ZtOd8JVsjSTa/Gydn66wKBgHKU
OmE6fIhSjTmejwibRYtz59S5AUgulwR2pA7rxbqEg0zoOLXIAqe+A77gqE6cesdf
SYToIPP13ee3OkLpXaz7EwvdwyhFypl6hqBKHec2DQRO00lU3Bo9opVydEhqqbbF
tnubpa8P4je5Ec1LMFpiWdzrXaJsT4VmdaqCiECBAoGBAJgxMGiIkKYpJU4g8J1t
5EIIPMGUKwuKbN1ZW16zh1GwkVLXevebXlnBgPMQKjdnAxHM3OuGP9ReP1aVpo4J
TaBvl+Fa9PtanirwnhYbAtNWoK5OqqSfXcdaLurSlyolZzirOpeNIwPJiJLAeuT5
VWsuyC89pTi3ww6iCSFOtuqm
-----END PRIVATE KEY-----

46
compare.js Normal file

@ -0,0 +1,46 @@
import fs from 'fs/promises';
const filePath1 = './data/colorcrew_data_transformed.json.bkup2';
const filePath2 = './data/colorcrew_data_transformed_prod.json';
export async function compareFiles() {
try {
// Read the files from the hardcoded paths
const json1 = JSON.parse(await fs.readFile(filePath1, 'utf8'));
const json2 = JSON.parse(await fs.readFile(filePath2, 'utf8'));
const result = compareTimeIntervals(json1, json2);
if (result.length > 0) {
console.log('Differences in break hours:', JSON.stringify(result, null, 2));
console.log(`Total differences: ${result.length}`);
} else {
console.log('No differences in break hours for matching start and end times.');
}
} catch (error) {
console.error('Error reading or parsing files:', error);
}
}
// Function to compare arrays of time intervals
export function compareTimeIntervals(arr1, arr2) {
const differences = [];
arr1.forEach((item1) => {
const matchingItem = arr2.find(item2 => item1.start_time === item2.start_time && item1.end_time === item2.end_time);
if (matchingItem) {
if (item1.break_hours !== matchingItem.break_hours) {
differences.push({
start_time: item1.start_time,
end_time: item1.end_time,
break_hours_in_first_file: item1.break_hours,
break_hours_in_second_file: matchingItem.break_hours
});
}
}
});
return differences;
}
compareFiles();

54
compare.test.js Normal file

@ -0,0 +1,54 @@
import { compareTimeIntervals } from './compare.js';
import chalk from 'chalk';
// Test 1: Dummy data with differences in break_hours
const jsonWithDifferences1 = [
{
"start_time": "12:15",
"end_time": "18:00",
"break_hours": "0.25"
}
];
const jsonWithDifferences2 = [
{
"start_time": "12:15",
"end_time": "18:00",
"break_hours": "0.50" // Different break_hours
}
];
// Test 2: Dummy data with no differences in break_hours
const jsonWithoutDifferences1 = [
{
"start_time": "12:15",
"end_time": "18:00",
"break_hours": "0.25"
}
];
const jsonWithoutDifferences2 = [
{
"start_time": "12:15",
"end_time": "18:00",
"break_hours": "0.25" // Same break_hours
}
];
// Function to run a single test with result expectations
function runTest(testName, json1, json2, shouldFindDifference) {
const result = compareTimeIntervals(json1, json2);
// Log the test name, result, and total number of differences found
if ((result.length > 0 && shouldFindDifference) || (result.length === 0 && !shouldFindDifference)) {
console.log(`Test: ${testName} - ${chalk.green('OK')}`);
console.log(`Total differences: ${result.length}`);
} else {
console.log(`Test: ${testName} - ${chalk.red('FAILED')}`);
console.log(`Total differences: ${result.length}`);
}
}
// Run tests
runTest("Test with Differences", jsonWithDifferences1, jsonWithDifferences2, true); // Should expect 1 difference, return OK
runTest("Test without Differences", jsonWithoutDifferences1, jsonWithoutDifferences2, false); // Should expect 0 differences, return OK

1192
data/colorcrew_data.csv Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1895
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file

@ -0,0 +1,25 @@
{
"name": "colorcrew_breakhour-_transform",
"version": "1.0.0",
"description": "",
"main": "server.js",
"type": "module",
"scripts": {
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^5.3.0",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"node-cron": "^3.0.3",
"node-fetch": "^3.3.2",
"open": "^10.1.0",
"pg": "^8.12.0"
}
}

332
public/index.html Normal file

@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Break Hour Processing</title>
<script>
document.addEventListener('DOMContentLoaded', () => {
// const url = 'https://breakhours.fleks.c7a.nl'
const url = 'https://localhost:22000'
const shiftsUrl = `${url}/api/shifts`;
const timeTableUrl = `${url}/api/time-table`;
const postUrl = `${url}/api/shift-update`;
const putUrl = `${url}/api/shifts`;
document.getElementById('refresh-data').addEventListener('click', async () => {
const apiKey = document.getElementById('api-key').value;
const userName = document.getElementById('username').value;
const passWord = document.getElementById('password').value;
if (!userName || !passWord || !apiKey) {
alert('Please enter username, password and apiKey');
return;
}
try {
const shiftsData = await fetchData('POST', shiftsUrl, apiKey);
const timeTable = await fetchData('GET', timeTableUrl);
const resultContainer = document.getElementById('result');
resultContainer.innerHTML = '';
const summaryStats = { totalNeedsChange: 0, totalDoesNotNeedChange: 0, totalNoMatch: 0 };
const shiftsTable = createShiftsTable(shiftsData, timeTable, summaryStats);
const statsTable = createStatsTable(summaryStats, shiftsData.results.length);
resultContainer.appendChild(statsTable);
resultContainer.appendChild(shiftsTable);
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('click', () => {
const checkboxes = document.querySelectorAll('.shift-row.needs-change input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.checked = true);
});
}
} catch (error) {
console.error('Failed to fetch data:', error);
alert('Error fetching data. Check the console for more details.');
}
});
document.addEventListener('click', async (event) => {
if (event.target.classList.contains('post-button')) {
const row = event.target.closest('tr');
const shiftUuid = event.target.dataset.uuid;
const newBreakMinutes = parseInt(row.cells[7].textContent.split(' ')[0]); // Assuming new break hours are in the format "XX min"
const newBreakHours = minutesToHoursMinutes(newBreakMinutes); // Convert to h:mm format
const userName = document.getElementById('username').value;
const passWord = document.getElementById('password').value;
const apiKey = document.getElementById('api-key').value;
const limit = document.getElementById('limit').value || 0;
if (!userName || !passWord || !apiKey) {
alert('Please enter username, password and apiKey');
return;
}
const body = [
{ uuid: shiftUuid, break_hours: newBreakHours, userName, passWord, limit: null }
];
try {
await fetch(putUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
alert(`Break hour update for ${shiftUuid} was successful.`);
} catch (error) {
alert(`Break hour update for ${shiftUuid} failed.`);
}
}
});
document.getElementById('queue-requests').addEventListener('click', async () => {
const userName = document.getElementById('username').value;
const passWord = document.getElementById('password').value;
const apiKey = document.getElementById('api-key').value;
const limit = 0 // document.getElementById('limit').value || 0;
if (!userName || !passWord || !apiKey) {
alert('Please enter username, password and apiKey');
return;
}
const config = { userName, passWord, apiKey, limit }
const selectedCheckboxes = document.querySelectorAll('.shift-row input[type="checkbox"]:checked');
const shifts = [];
selectedCheckboxes.forEach(checkbox => {
const row = checkbox.closest('tr');
const shiftUuid = checkbox.dataset.uuid;
const newBreakMinutes = parseInt(row.cells[7].textContent.split(' ')[0]);
const newBreakHours = minutesToHoursMinutes(newBreakMinutes);
shifts.push({ uuid: shiftUuid, break_hours: newBreakHours });
});
if (shifts.length === 0) {
alert('No shifts selected.');
return;
}
try {
await fetch(putUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config, shifts })
});
alert('Break hour updates were successful.');
} catch (error) {
alert('Break hour updates failed.');
}
});
});
function minutesToHoursMinutes(minutes) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}:${mins.toString().padStart(2, '0')}`;
}
async function fetchData(method, url, apiKey) {
const options = {
method: method || 'GET',
headers: { 'Content-Type': 'application/json' },
};
if (method === 'POST') {
options.body = JSON.stringify({ apiKey })
}
const response = await fetch(url, options);
if (!response.ok) throw new Error('Network response was not ok.');
return await response.json();
}
function createShiftsTable(shiftsData, timeTable, summaryStats) {
const sortedShifts = shiftsData.results.sort((a, b) => a.project_title.localeCompare(b.project_title));
const table = document.createElement('table');
table.innerHTML = `
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>Shift UUID</th>
<th>Job ID</th>
<th>Project Title</th>
<th>Start Time</th>
<th>End Time</th>
<th>Old Break Hours (Decimal/Minutes)</th>
<th>New Break Hours</th>
<th>Post</th>
</tr>`;
sortedShifts.forEach(shift => {
const row = createShiftRow(shift, timeTable, summaryStats);
table.appendChild(row);
});
return table;
}
function createShiftRow(shift, timeTable, summaryStats) {
const startTime = shift.start_time.slice(0, 5);
const endTime = shift.end_time.slice(0, 5);
const matchedTime = timeTable.find(entry =>
entry.start_time.replace('.', ':') === startTime &&
entry.end_time.replace('.', ':') === endTime
);
const oldBreakDecimal = parseFloat(shift.break_compensation);
const oldBreakMinutes = Math.round(oldBreakDecimal * 60);
const newBreakMinutes = matchedTime ? Math.round(parseFloat(matchedTime.break_hours) * 60) : "N/A";
const statusColor = newBreakMinutes === "N/A" ? 'red' : (oldBreakMinutes === newBreakMinutes ? 'green' : 'orange');
const statusClass = newBreakMinutes === "N/A" ? 'no-match' : (oldBreakMinutes === newBreakMinutes ? 'no-change' : 'needs-change');
updateSummaryStats(newBreakMinutes, oldBreakMinutes, summaryStats);
const tr = document.createElement('tr');
tr.classList.add('shift-row', statusClass);
tr.innerHTML = `
<td><input type="checkbox" data-uuid="${shift.shifts_uuid}"></td>
<td>${shift.shifts_uuid}</td>
<td>${shift.job_id}</td>
<td>${shift.project_title}</td>
<td>${startTime}</td>
<td>${endTime}</td>
<td>${oldBreakDecimal.toFixed(2)} / ${oldBreakMinutes} min</td>
<td style="background-color: ${statusColor}">${newBreakMinutes} min</td>
<td><button class="post-button" data-uuid="${shift.shifts_uuid}">Post</button></td>
`;
return tr;
}
function updateSummaryStats(newBreakMinutes, oldBreakMinutes, summaryStats) {
if (newBreakMinutes === "N/A") summaryStats.totalNoMatch++;
else if (oldBreakMinutes === newBreakMinutes) summaryStats.totalDoesNotNeedChange++;
else summaryStats.totalNeedsChange++;
}
function createStatsTable(summaryStats, totalShifts) {
const table = document.createElement('table');
table.classList.add('stats-table');
table.innerHTML = `<tr><th>Type</th><th>Count</th><th>Show</th></tr>
<tr>
<td>Needs Change</td><td>${summaryStats.totalNeedsChange}</td>
<td><input type="checkbox" checked onclick="toggleVisibility('needs-change', this.checked)"></td>
</tr>
<tr>
<td>Does Not Need Change</td><td>${summaryStats.totalDoesNotNeedChange}</td>
<td><input type="checkbox" checked onclick="toggleVisibility('no-change', this.checked)"></td>
</tr>
<tr>
<td>No Match</td><td>${summaryStats.totalNoMatch}</td>
<td><input type="checkbox" checked onclick="toggleVisibility('no-match', this.checked)"></td>
</tr>
<tr><th>Total Shifts</th><th colspan="2">${totalShifts}</th></tr>`;
return table;
}
function toggleVisibility(className, isVisible) {
const rows = document.querySelectorAll(`.${className}`);
rows.forEach(row => {
row.style.display = isVisible ? '' : 'none';
});
}
</script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.inputs-container {
display: flex;
align-items: center;
gap: 10px;
}
.inputs-container input {
flex: 1;
padding: 5px;
font-size: 1em;
}
.button-container {
display: flex;
align-items: center;
gap: 10px;
}
.button-container button {
padding: 5px 10px;
font-size: 1em;
width: 150px;
}
table,
th,
td {
border: 1px solid black;
border-collapse: collapse;
margin: 10px 0;
width: 100%;
table-layout: fixed;
}
th,
td {
padding: 10px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
td:nth-child(1),
th:nth-child(1) {
width: 11.11%;
}
.stats-table th:nth-child(1),
.stats-table td:nth-child(1) {
width: 50px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>Break Hour Processing</h1>
<div class="inputs-container">
<input type="text" id="username" placeholder="Enter your username" size="30">
<input type="password" id="password" placeholder="Enter your password" size="30">
<input type="text" id="api-key" placeholder="Enter your API key here" size="30">
</div>
<div class="button-container">
<button id="refresh-data">Refresh Data</button>
<button id="queue-requests">Queue Requests</button>
</div>
<div id="result"></div>
</div>
</body>
</html>

16
router.js Normal file

@ -0,0 +1,16 @@
import express from 'express';
import { getJobs, processAndUpdateJobs, removePauzeFromJobs } from './api/jobs.js';
import { getShifts, updateShifts } from './api/shifts.js';
const router = express.Router();
// Jobs
router.get('/jobs', getJobs);
router.post('/jobs/process', processAndUpdateJobs);
router.post('/jobs/remove-pauze', removePauzeFromJobs);
// Shifts
router.post('/shifts', getShifts);
router.put('/shifts', updateShifts);
export default router;

64
server.js Normal file

@ -0,0 +1,64 @@
import express from 'express';
import fetch from 'node-fetch';
import { promises as fs } from 'fs';
import http from 'http';
import https from 'https';
import cors from 'cors';
import open from 'open';
import path from 'path';
import { fileURLToPath } from 'url';
import router from './router.js'
import { login } from './api/auth.js'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const port = 22000;
app.use(cors())
app.use(express.json({ limit: '50mb' }));
app.use(express.static('data'));
app.use(express.static('public'));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/api/time-table', (req, res) => {
res.sendFile(path.join(__dirname, 'data', 'colorcrew_data_transformed_prod.json'));
});
///////////
// Routes
// auth
app.post('/api/login', async (req, res) => {
const { userName, passWord } = req.body
const token = await login(userName, passWord)
if (token) {
res.status(200).send({ token })
} else {
res.status(403).send({ error: 'token not found' })
}
})
app.use('/api', router);
async function startServer() {
try {
const options = {
key: await fs.readFile('./certs/server.key'),
cert: await fs.readFile('./certs/server.cert')
};
https.createServer(options, app).listen(port, () => {
console.log(`HTTPS Server running at https://localhost:${port}`);
// open(`https://localhost:${port}`);
});
} catch (err) {
console.error('Error starting HTTPS server:', err);
}
}
startServer();

207
services/PauzeManager.js Normal file

@ -0,0 +1,207 @@
import { query } from './db.js';
export class PauzeManager {
constructor(identifier = 'Pauze: ', suffix = '') {
this.identifier = identifier;
this.suffix = suffix;
this.pauzeRegex = new RegExp(`${this.identifier}\\d+\\s*min\\.?\\s*(--\\s*)?`);
}
parseTime(timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
return { hours, minutes };
}
isMatchingTime(jobStart, jobEnd, entryStart, entryEnd) {
const jobStartTime = this.parseTime(jobStart);
const jobEndTime = this.parseTime(jobEnd);
const entryStartTime = this.parseTime(entryStart);
const entryEndTime = this.parseTime(entryEnd);
const jobStartMinutes = jobStartTime.hours * 60 + jobStartTime.minutes;
const jobEndMinutes = jobEndTime.hours * 60 + jobEndTime.minutes;
const entryStartMinutes = entryStartTime.hours * 60 + entryStartTime.minutes;
const entryEndMinutes = entryEndTime.hours * 60 + entryEndTime.minutes;
const normalizedJobEndMinutes = jobEndMinutes < jobStartMinutes ? jobEndMinutes + 24 * 60 : jobEndMinutes;
const normalizedEntryEndMinutes = entryEndMinutes < entryStartMinutes ? entryEndMinutes + 24 * 60 : entryEndMinutes;
return jobStartMinutes === entryStartMinutes && normalizedJobEndMinutes === normalizedEntryEndMinutes;
}
convertBreakHoursToPauze(breakHours) {
const hours = parseFloat(breakHours);
const minutes = Math.round(hours * 60);
return `${this.identifier}${minutes} min${this.suffix}`;
}
updateFunctionDescription(description, pauzeText) {
if (!description) return pauzeText;
const match = description.match(this.pauzeRegex);
if (match) {
const trailingDashes = match[0].includes('--') ? ' -- ' : '';
return description.replace(this.pauzeRegex, `${pauzeText}${trailingDashes}`);
} else {
return `${pauzeText}${description.trim() ? ' -- ' + description : ''}`;
}
}
// updateFunctionDescription(description, pauzeText) {
// if (!description) return pauzeText;
// if (this.pauzeRegex.test(description)) {
// return description.replace(this.pauzeRegex, pauzeText);
// } else {
// return `${pauzeText}${description.trim() ? ' -- ' + description : ''}`;
// }
// }
updateJobsWithPauze(jobs, timeTable, convertToAmsterdamTime) {
return jobs.map(job => {
const startDate = new Date(job.start_date);
const endDate = new Date(job.end_date);
const startAmsterdamTime = convertToAmsterdamTime(startDate);
const endAmsterdamTime = convertToAmsterdamTime(endDate);
const formattedStartTime = startAmsterdamTime.slice(0, 5);
const formattedEndTime = endAmsterdamTime.slice(0, 5);
const matchedTime = timeTable.find(entry =>
this.isMatchingTime(formattedStartTime, formattedEndTime, entry.start_time, entry.end_time)
);
const oldBreakDecimal = parseFloat(job.break_hours);
const oldBreakMinutes = Math.round(oldBreakDecimal * 60);
const newBreakMinutes = matchedTime ? Math.round(parseFloat(matchedTime.break_hours) * 60) : "N/A";
let updatedDescription = job.function_description;
if (matchedTime) {
const pauzeText = this.convertBreakHoursToPauze(matchedTime.break_hours);
updatedDescription = this.updateFunctionDescription(job.function_description, pauzeText);
}
return {
job_id: job.job_id,
job_uuid: job.job_uuid,
project_title: job.project_title,
start_time: startAmsterdamTime,
end_time: endAmsterdamTime,
function_description: updatedDescription,
break_minutes: {
old: oldBreakMinutes,
new: newBreakMinutes
}
};
});
}
async updateJobsInDatabase(tenantId, jobs) {
if (!tenantId) {
throw new Error("Tenant ID is required");
}
const updatePromises = jobs.map(job => {
const queryText = `
UPDATE public.jobs_jobs
SET function_description = $2
WHERE uuid = $3
AND tenant_id = $1
`;
const queryValues = [
tenantId,
job.function_description,
job.job_uuid
];
return query(queryText, queryValues);
});
await Promise.all(updatePromises);
}
async getAllJobs(tenantId) {
const queryText = `
SELECT *
FROM public.jobs_jobs
WHERE tenant_id = $1
`;
const queryValues = [tenantId];
const res = await query(queryText, queryValues);
return res.rows;
}
async getJobByUUID(jobUUID) {
const queryText = `
SELECT *
FROM public.jobs_jobs
WHERE uuid = $1
`;
const queryValues = [jobUUID];
const res = await query(queryText, queryValues);
return res.rows[0];
}
async getAllJobsWithPauze() {
const queryText = `
SELECT *
FROM public.jobs_jobs
WHERE function_description LIKE $1
`;
const queryValues = [`%${this.identifier}%`];
const res = await query(queryText, queryValues);
return res.rows;
}
async getJobsWithPauze(tenantId) {
const queryText = `
SELECT *
FROM public.jobs_jobs
WHERE tenant_id = $1 AND function_description LIKE '%Pauze%'
`;
const queryValues = [tenantId];
const res = await query(queryText, queryValues);
return res.rows;
}
async removePauzeFromDatabase(tenantId) {
if (!tenantId) {
throw new Error("Tenant ID is required");
}
const jobsWithPauze = await this.getAllJobsWithPauze();
console.log({ jobspauze: jobsWithPauze[0] })
const jobsWithoutPauze = jobsWithPauze.map(job => {
let description = job.function_description;
if (!description) return job;
// Remove the pauze text
let updatedDescription = description.replace(this.pauzeRegex, '').trim();
if (updatedDescription.startsWith('--')) {
updatedDescription = updatedDescription.slice(2).trim();
}
if (updatedDescription.endsWith('--')) {
updatedDescription = updatedDescription.slice(0, -2).trim();
}
return {
...job,
function_description: updatedDescription
};
});
await this.updateJobsInDatabase(tenantId, jobsWithoutPauze);
}
}

25
services/db.js Normal file

@ -0,0 +1,25 @@
import pkg from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const { Pool } = pkg;
const pool = new Pool({
user: process.env.PSQL_USERNAME,
host: process.env.PSQL_HOSTNAME,
database: process.env.PSQL_DATABASE,
password: process.env.PSQL_PASSWORD,
port: process.env.PSQL_PORT,
});
pool.on('connect', () => {
console.log('Connected to the database');
});
pool.on('error', (err, client) => {
console.error('Unknown database error', err);
process.exit(-1);
});
export const query = (text, params) => pool.query(text, params);

36
transform.js Normal file

@ -0,0 +1,36 @@
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
function formatTime(decimalTime) {
const [hours, minutes] = decimalTime.split('.');
return `${hours.padStart(2, '0')}:${(minutes || '0').padEnd(2, '0')}`;
}
function csvToJson(filePath) {
const data = fs.readFileSync(filePath, 'utf8');
const rows = data.trim().split('\n');
const result = rows.map(row => {
const parts = row.split(',');
const start_time = formatTime(parts[0]);
const end_time = formatTime(parts[1]);
const break_hours = parseFloat(parts[2]).toFixed(2);
return {
start_time,
end_time,
break_hours
};
});
return JSON.stringify(result, null, 4);
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const csvFilePath = path.join(__dirname, './data/colorcrew_data.csv');
const jsonOutput = csvToJson(csvFilePath);
const outputFilePath = path.join(__dirname, './data/colorcrew_data_transformed.json');
fs.writeFileSync(outputFilePath, jsonOutput, 'utf8');