192 lines
7.5 KiB
JavaScript
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 };
|