Initial commit — Mek-Tech AI Consultancy Framework v2.0
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user