const { useState: useStateLive, useMemo: useMemoLive, useEffect: useEffectLive, useRef: useRefLive } = React;
const STATUS_BUTTONS = [
{ id: 'Open', label: 'Open' },
{ id: 'Working', label: 'Bezig' },
{ id: 'Completed', label: 'Klaar' },
];
const TEMPLATE_PRIORITY_OPTIONS_LIVE = ['Low', 'Medium', 'High', 'Urgent'];
const statusButtonStyle = (active) => ({
padding: '5px 9px',
fontSize: 11,
fontWeight: 600,
borderRadius: 6,
cursor: 'pointer',
border: active ? '1px solid var(--ink-1)' : '1px solid var(--line)',
background: active ? 'var(--ink-1)' : 'var(--paper)',
color: active ? 'var(--paper)' : 'var(--ink-2)',
});
const inputBaseStyle = {
width: '100%',
padding: '8px 10px',
fontSize: 13,
border: '1px solid var(--line)',
borderRadius: 7,
background: 'var(--paper)',
color: 'var(--ink-1)',
fontFamily: 'inherit',
outline: 'none',
boxSizing: 'border-box',
};
const compactDateInputStyle = {
...inputBaseStyle,
width: 150,
padding: '6px 8px',
fontSize: 11.5,
};
const emptyAddress = () => ({
address_line1: '',
address_line2: '',
city: '',
county: '',
state: '',
pincode: '',
country: 'Netherlands',
});
const withAddressDefaults = (address) => ({
...emptyAddress(),
...(address || {}),
});
const emptyContactDraft = () => ({
name: '',
email: '',
phone: '',
role: '',
});
const emptyUserDraft = () => ({
name: '',
email: '',
phone: '',
mobile: '',
roleProfile: '',
});
const makeDraftId = () => `draft-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const emptyTemplateStepDraft = (taskType = '') => ({
localId: makeDraftId(),
taskId: '',
taskType,
name: taskType || '',
durationDays: 1,
expectedHours: '',
priority: 'Medium',
milestone: false,
description: '',
});
const parseNumericInput = (value, fallback = 0) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const formatHoursLabel = (value) => {
const amount = parseNumericInput(value, 0);
if (!amount) return '0 uur';
const rounded = Math.round(amount * 10) / 10;
return `${String(rounded).replace('.0', '')} uur`;
};
const formatAddressPreview = (address) => {
const normalized = withAddressDefaults(address);
return [
normalized.address_line1,
normalized.address_line2,
[normalized.pincode, normalized.city].filter(Boolean).join(' ').trim(),
normalized.country,
].filter(Boolean);
};
const displayTeamMemberName = (user) => {
const cleaned = String(user?.naam || '').replace(/^Pilot\s+/i, '').trim();
return cleaned || user?.naam || user?.id || 'Onbekend';
};
const emptyWorkloadStats = () => ({
open: 0,
overdue: 0,
deze_week: 0,
vandaag: 0,
});
const isWorkdayLive = (date) => {
const weekday = date.getDay();
return weekday >= 1 && weekday <= 5;
};
const shiftToWorkdayLive = (date, direction = 'next') => {
if (!date) return null;
const cursor = new Date(date);
cursor.setHours(0, 0, 0, 0);
const step = direction === 'previous' ? -1 : 1;
while (!isWorkdayLive(cursor)) {
cursor.setDate(cursor.getDate() + step);
}
return cursor;
};
const addWorkdaysLive = (date, amount) => {
const cursor = shiftToWorkdayLive(date, 'next');
let remaining = Math.max(parseNumericInput(amount, 0), 0);
while (remaining > 0) {
cursor.setDate(cursor.getDate() + 1);
if (isWorkdayLive(cursor)) remaining -= 1;
}
return cursor;
};
const buildTemplateSchedulePreview = (template, startDateValue) => {
if (!template || !(template.steps || []).length) return null;
const baseStart = shiftToWorkdayLive(startDateValue || window.todayDate || new Date(), 'next');
let currentStart = new Date(baseStart);
let lastEnd = new Date(baseStart);
const stepSchedule = template.steps.map((step) => {
const durationDays = Math.max(parseNumericInput(step.durationDays, 1), 1);
const start = new Date(currentStart);
const end = addWorkdaysLive(start, durationDays - 1);
lastEnd = new Date(end);
currentStart = addWorkdaysLive(end, 1);
return {
...step,
start: window.iso(start),
end: window.iso(end),
durationDays,
};
});
return {
start: window.iso(baseStart),
end: window.iso(lastEnd),
stepCount: stepSchedule.length,
durationDays: stepSchedule.reduce((sum, step) => sum + step.durationDays, 0),
expectedHours: stepSchedule.reduce((sum, step) => sum + parseNumericInput(step.expectedHours, 0), 0),
steps: stepSchedule,
};
};
function getMyDayAgendaSnapshotLive(userId) {
const today = String(window.today || window.iso(new Date()));
const tasks = window.getMyTasks(userId);
const prepared = tasks.map((task) => {
const start = task.start || task.deadline || '';
const deadline = task.deadline || task.start || '';
const overdue = Boolean(deadline && window.daysUntil(deadline) < 0);
const activeToday = Boolean(start && deadline && start <= today && deadline >= today);
const anchorDate = activeToday ? today : (start || deadline || '');
return {
...task,
start,
deadline,
overdue,
anchorDate,
};
});
const overdueTasks = prepared.filter((task) => task.overdue);
const plannedTasks = prepared.filter((task) => !task.overdue);
const timelineKeys = [
...new Set(
plannedTasks
.filter((task) => task.anchorDate)
.map((task) => task.anchorDate)
.sort()
),
].slice(0, 5);
const visiblePlannedTasks = plannedTasks.filter((task) => timelineKeys.includes(task.anchorDate));
return {
allCount: tasks.length,
visibleCount: overdueTasks.length + visiblePlannedTasks.length,
};
}
function buildSidebarSections() {
const myDaySnapshot = getMyDayAgendaSnapshotLive(window.CURRENT_USER_ID);
return [
{
title: null,
items: [
{ id: 'kanban', label: 'Kanban', icon: 'kanban', count: window.PROJECTEN.length },
{ id: 'mijndag', label: 'Mijn dag', icon: 'today', count: myDaySnapshot.visibleCount, hot: myDaySnapshot.visibleCount > 0 },
{ id: 'perklant', label: 'Per klant', icon: 'klant' },
{ id: 'lijst', label: 'Alle projecten', icon: 'lijst' },
{ id: 'team', label: 'Team workload', icon: 'team' },
],
},
{
title: 'Configuratie',
items: [
{ id: 'klanten', label: 'Klanten', icon: 'klant', count: window.KLANTEN.length },
{ id: 'gebruikers', label: 'Gebruikers', icon: 'team', count: window.GEBRUIKERS.length },
{ id: 'templates', label: 'Templates', icon: 'settings', count: window.PROJECT_TEMPLATES.filter((template) => !template.disabled).length },
],
},
];
}
const SidebarLive = ({ view, setView, onNewProject }) => {
const currentUser = window.medewerkerById(window.CURRENT_USER_ID);
const currentConfigUser = window.GEBRUIKERS.find((user) => user.id === window.CURRENT_USER_ID) || null;
const [switchingUser, setSwitchingUser] = useStateLive(false);
const [switchError, setSwitchError] = useStateLive('');
const [userMenuOpen, setUserMenuOpen] = useStateLive(false);
const userMenuRef = useRefLive(null);
const switchableUsers = useMemoLive(() => (
[...window.GEBRUIKERS].sort((left, right) => (
String(left.naam || left.id).localeCompare(String(right.naam || right.id), 'nl')
))
), [window.GEBRUIKERS]);
useEffectLive(() => {
if (!userMenuOpen) return undefined;
const handleOutsideClick = (event) => {
if (userMenuRef.current && !userMenuRef.current.contains(event.target)) {
setUserMenuOpen(false);
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => document.removeEventListener('mousedown', handleOutsideClick);
}, [userMenuOpen]);
const handleUserSwitch = async (nextUserId) => {
if (!nextUserId) return;
if (nextUserId === window.CURRENT_USER_ID) {
setUserMenuOpen(false);
return;
}
setSwitchingUser(true);
setSwitchError('');
try {
await window.dashboardApi.switchCurrentUser(nextUserId);
setUserMenuOpen(false);
} catch (error) {
setSwitchError(error.message || 'Wisselen van gebruiker mislukt.');
} finally {
setSwitchingUser(false);
}
};
return (
);
};
const TopbarLive = ({ view, search, setSearch }) => {
const myDaySnapshot = getMyDayAgendaSnapshotLive(window.CURRENT_USER_ID);
const titles = {
kanban: 'NPD Ontwikkeling',
mijndag: 'Mijn dag',
perklant: 'Per klant',
klanten: 'Klanten',
gebruikers: 'Gebruikers',
templates: 'Projecttemplates',
lijst: 'Alle projecten',
team: 'Team workload',
};
const subtitles = {
kanban: `${window.PROJECTEN.length} lopende projecten · ${window.BUCKETS.length} buckets`,
mijndag: `${myDaySnapshot.visibleCount} taken in beeld · ${window.todayHuman}`,
perklant: `${window.KLANTEN.length} klanten met lopende of potentiële dashboardprojecten`,
klanten: '',
gebruikers: '',
templates: `${window.PROJECT_TEMPLATES.length} templates voor automatische stapplanning`,
lijst: 'Zoek, filter en demo vanuit live ERPNext-data',
team: `${window.ALL_TASKS.length} openstaande stappen`,
};
return (
{titles[view]}