Files
mek-tech-consulting/advice.js
T

192 lines
7.5 KiB
JavaScript

const { getDb } = require('./db');
const ADVICE_ACTIONS = {
lead_followup: (id) => `/clients/${id}`,
initial_assessment: (id) => `/consulting?client_id=${id}`,
reassessment: (id) => `/consulting?client_id=${id}`,
start_engagement: (id) => `/engagements/new/${id}`,
next_phase: (id) => `/clients/${id}`,
architecture_phase: (id) => `/architecture?client_id=${id}`,
implementation_phase: (id) => `/engagements/new/${id}`,
monitoring_phase: (id) => `/engagements/new/${id}`,
training: (id) => `/engagements/new/${id}`,
overdue_tasks: (id) => `/clients/${id}`,
no_diagram: (id) => id ? `/architecture/new/${id}` : '/architecture/new',
data_quality: (id) => `/quality?client_id=${id}`,
ai_readiness: () => '/ai',
no_time_logged: (id) => `/time?client_id=${id}`,
stale_engagement: (id) => `/clients/${id}`,
leadgen_opportunity: (id) => `/?tab=leadgen`,
proposal_ready: (id) => `/consulting/report/${id}`,
};
function adviceAction(type, clientId, extra) {
const fn = ADVICE_ACTIONS[type];
if (!fn) return clientId ? `/clients/${clientId}` : '/clients';
return fn(clientId, extra);
}
function generateAdvice(clientId) {
const db = getDb();
const client = db.prepare('SELECT * FROM clients WHERE id = ?').get(clientId);
if (!client) return [];
const engagementCount = db.prepare('SELECT COUNT(*) as cnt FROM engagements WHERE client_id = ?').get(clientId);
const lastAssessment = db.prepare('SELECT * FROM assessments WHERE client_id = ? ORDER BY created_at DESC').get(clientId);
const activeEngagements = db.prepare("SELECT * FROM engagements WHERE client_id = ? AND status = 'in_progress'").all(clientId);
const completedEngagements = db.prepare("SELECT * FROM engagements WHERE client_id = ? AND status = 'completed'").all(clientId);
const diagramCount = db.prepare('SELECT COUNT(*) as cnt FROM data_diagrams WHERE client_id = ?').get(clientId);
const timeLogged = db.prepare('SELECT COUNT(*) as cnt FROM time_entries WHERE client_id = ?').get(clientId);
const existingAdvice = db.prepare("SELECT advice_type FROM advice_log WHERE client_id = ? AND dismissed = 0").all(clientId);
const existing = new Set(existingAdvice.map(a => a.advice_type));
const advice = [];
function checkAdvice(type, text, priority) {
if (existing.has(type)) return;
advice.push({
client_id: clientId,
client_name: client.name,
advice_type: type,
advice_text: text,
priority: priority || 'medium',
action_url: adviceAction(type, clientId, lastAssessment?.id),
});
}
if (client.status === 'lead') {
checkAdvice('lead_followup',
`${client.name} is nog een lead. Plan een intakegesprek en start het Enterprise Framework assessment.`,
'high');
}
if (!lastAssessment) {
checkAdvice('initial_assessment',
`Start een eerste assessment voor ${client.name}: infra, data, AI-maturity en roadmap.`,
'critical');
}
if (lastAssessment) {
const daysSince = Math.floor((Date.now() - new Date(lastAssessment.created_at).getTime()) / (1000 * 60 * 60 * 24));
if (daysSince > 90) {
checkAdvice('reassessment',
`Laatste assessment voor ${client.name} was ${daysSince} dagen geleden. Tijd voor her-evaluatie.`,
'medium');
}
if (diagramCount.cnt === 0) {
checkAdvice('no_diagram',
`Assessment afgerond voor ${client.name}, maar nog geen architectuurdiagram. Maak een to-be design.`,
'high');
}
}
if (client.status === 'active' && engagementCount.cnt === 0) {
checkAdvice('start_engagement',
`${client.name} is actief maar heeft geen engagements. Start een discovery- of designtraject.`,
'high');
}
if (client.status === 'active' && activeEngagements.length === 0 && completedEngagements.length > 0) {
checkAdvice('next_phase',
`${client.name} heeft geen lopende projecten. Bespreek volgende fase: monitoring, AI Ops of opschaling.`,
'high');
}
const hasArchitecture = [...completedEngagements, ...activeEngagements].some(e => e.type === 'architecture');
const hasImplementation = [...completedEngagements, ...activeEngagements].some(e => e.type === 'implementation');
const hasMonitoring = [...completedEngagements, ...activeEngagements].some(e => e.type === 'monitoring');
const hasConsulting = [...completedEngagements, ...activeEngagements].some(e => e.type === 'consulting');
if (lastAssessment && !hasArchitecture && !hasConsulting) {
checkAdvice('architecture_phase',
`Assessment klaar voor ${client.name}. Start architectuur- of design engagement.`,
'high');
}
if (hasArchitecture && !hasImplementation) {
checkAdvice('implementation_phase',
`Architectuur gereed voor ${client.name}. Start implementatiefase of POC.`,
'high');
}
if (hasImplementation && !hasMonitoring) {
checkAdvice('monitoring_phase',
`Implementatie afgerond bij ${client.name}. Richt monitoring & observability in.`,
'medium');
}
const hasTraining = [...completedEngagements, ...activeEngagements].some(e => e.type === 'training');
if (hasImplementation && !hasTraining) {
checkAdvice('training',
`${client.name} heeft implementatie gehad. Bied training & knowledge transfer aan.`,
'low');
}
if (lastAssessment && diagramCount.cnt > 0) {
checkAdvice('data_quality',
`Voer een data quality scan uit voor ${client.name} ter onderbouwing van het design.`,
'medium');
}
if (lastAssessment && client.status === 'active') {
checkAdvice('ai_readiness',
`Beoordeel AI/LLM readiness voor ${client.name} en configureer de juiste provider.`,
'low');
}
if (client.status === 'active' && timeLogged.cnt === 0 && engagementCount.cnt > 0) {
checkAdvice('no_time_logged',
`Nog geen uren geregistreerd voor ${client.name}. Log tijd voor facturatie & projectinzicht.`,
'medium');
}
for (const eng of activeEngagements) {
const daysSinceUpdate = Math.floor((Date.now() - new Date(eng.updated_at || eng.created_at).getTime()) / (1000 * 60 * 60 * 24));
if (daysSinceUpdate > 14) {
checkAdvice('stale_engagement',
`Engagement "${eng.title}" bij ${client.name} is ${daysSinceUpdate} dagen niet bijgewerkt. Plan een statusreview.`,
'medium');
break;
}
}
const allTasks = db.prepare(`
SELECT t.* FROM tasks t
JOIN engagements e ON e.id = t.engagement_id
WHERE e.client_id = ? AND t.status IN ('todo','in_progress')
`).all(clientId);
if (allTasks.length > 0) {
const overdue = allTasks.filter(t => t.due_date && new Date(t.due_date) < new Date());
if (overdue.length > 0) {
checkAdvice('overdue_tasks',
`${overdue.length} ta(a)k(en) voor ${client.name} zijn verlopen. Herprioriteer en communiceer.`,
'critical');
}
}
if (client.status === 'lead' && !client.website) {
checkAdvice('leadgen_opportunity',
`Voeg een website toe voor ${client.name} en scan met LeadGen AI op tech signals.`,
'low');
}
return advice;
}
function syncAdviceToDb(clientId, adviceItems) {
if (!adviceItems || adviceItems.length === 0) return;
const db = getDb();
const insert = db.prepare('INSERT INTO advice_log (client_id, advice_type, advice_text, priority) VALUES (?, ?, ?, ?)');
for (const a of adviceItems) {
try {
insert.run(clientId, a.advice_type, a.advice_text, a.priority || 'medium');
} catch (e) {}
}
}
function getAdviceActionUrl(type, clientId, assessmentId) {
return adviceAction(type, clientId, assessmentId);
}
module.exports = { generateAdvice, syncAdviceToDb, getAdviceActionUrl, ADVICE_ACTIONS };