@hasna/microservices 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project, Milestone, and Deliverable CRUD operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
|
|
9
|
+
export interface Project {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
client: string | null;
|
|
14
|
+
status: string;
|
|
15
|
+
budget: number | null;
|
|
16
|
+
spent: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
start_date: string | null;
|
|
19
|
+
end_date: string | null;
|
|
20
|
+
owner: string | null;
|
|
21
|
+
tags: string[];
|
|
22
|
+
metadata: Record<string, unknown>;
|
|
23
|
+
created_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProjectRow {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
client: string | null;
|
|
32
|
+
status: string;
|
|
33
|
+
budget: number | null;
|
|
34
|
+
spent: number;
|
|
35
|
+
currency: string;
|
|
36
|
+
start_date: string | null;
|
|
37
|
+
end_date: string | null;
|
|
38
|
+
owner: string | null;
|
|
39
|
+
tags: string;
|
|
40
|
+
metadata: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rowToProject(row: ProjectRow): Project {
|
|
46
|
+
return {
|
|
47
|
+
...row,
|
|
48
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
49
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Milestone {
|
|
54
|
+
id: string;
|
|
55
|
+
project_id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
description: string | null;
|
|
58
|
+
due_date: string | null;
|
|
59
|
+
status: string;
|
|
60
|
+
completed_at: string | null;
|
|
61
|
+
created_at: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Deliverable {
|
|
65
|
+
id: string;
|
|
66
|
+
milestone_id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
description: string | null;
|
|
69
|
+
status: string;
|
|
70
|
+
assignee: string | null;
|
|
71
|
+
due_date: string | null;
|
|
72
|
+
completed_at: string | null;
|
|
73
|
+
created_at: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Project CRUD ---
|
|
77
|
+
|
|
78
|
+
export interface CreateProjectInput {
|
|
79
|
+
name: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
client?: string;
|
|
82
|
+
status?: string;
|
|
83
|
+
budget?: number;
|
|
84
|
+
spent?: number;
|
|
85
|
+
currency?: string;
|
|
86
|
+
start_date?: string;
|
|
87
|
+
end_date?: string;
|
|
88
|
+
owner?: string;
|
|
89
|
+
tags?: string[];
|
|
90
|
+
metadata?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createProject(input: CreateProjectInput): Project {
|
|
94
|
+
const db = getDatabase();
|
|
95
|
+
const id = crypto.randomUUID();
|
|
96
|
+
const tags = JSON.stringify(input.tags || []);
|
|
97
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
98
|
+
|
|
99
|
+
db.prepare(
|
|
100
|
+
`INSERT INTO projects (id, name, description, client, status, budget, spent, currency, start_date, end_date, owner, tags, metadata)
|
|
101
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
102
|
+
).run(
|
|
103
|
+
id,
|
|
104
|
+
input.name,
|
|
105
|
+
input.description || null,
|
|
106
|
+
input.client || null,
|
|
107
|
+
input.status || "planning",
|
|
108
|
+
input.budget ?? null,
|
|
109
|
+
input.spent ?? 0,
|
|
110
|
+
input.currency || "USD",
|
|
111
|
+
input.start_date || null,
|
|
112
|
+
input.end_date || null,
|
|
113
|
+
input.owner || null,
|
|
114
|
+
tags,
|
|
115
|
+
metadata
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return getProject(id)!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getProject(id: string): Project | null {
|
|
122
|
+
const db = getDatabase();
|
|
123
|
+
const row = db.prepare("SELECT * FROM projects WHERE id = ?").get(id) as ProjectRow | null;
|
|
124
|
+
return row ? rowToProject(row) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ListProjectsOptions {
|
|
128
|
+
status?: string;
|
|
129
|
+
client?: string;
|
|
130
|
+
owner?: string;
|
|
131
|
+
search?: string;
|
|
132
|
+
limit?: number;
|
|
133
|
+
offset?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function listProjects(options: ListProjectsOptions = {}): Project[] {
|
|
137
|
+
const db = getDatabase();
|
|
138
|
+
const conditions: string[] = [];
|
|
139
|
+
const params: unknown[] = [];
|
|
140
|
+
|
|
141
|
+
if (options.status) {
|
|
142
|
+
conditions.push("status = ?");
|
|
143
|
+
params.push(options.status);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.client) {
|
|
147
|
+
conditions.push("client = ?");
|
|
148
|
+
params.push(options.client);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (options.owner) {
|
|
152
|
+
conditions.push("owner = ?");
|
|
153
|
+
params.push(options.owner);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.search) {
|
|
157
|
+
conditions.push(
|
|
158
|
+
"(name LIKE ? OR description LIKE ? OR client LIKE ? OR owner LIKE ?)"
|
|
159
|
+
);
|
|
160
|
+
const q = `%${options.search}%`;
|
|
161
|
+
params.push(q, q, q, q);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let sql = "SELECT * FROM projects";
|
|
165
|
+
if (conditions.length > 0) {
|
|
166
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
167
|
+
}
|
|
168
|
+
sql += " ORDER BY created_at DESC";
|
|
169
|
+
|
|
170
|
+
if (options.limit) {
|
|
171
|
+
sql += " LIMIT ?";
|
|
172
|
+
params.push(options.limit);
|
|
173
|
+
}
|
|
174
|
+
if (options.offset) {
|
|
175
|
+
sql += " OFFSET ?";
|
|
176
|
+
params.push(options.offset);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const rows = db.prepare(sql).all(...params) as ProjectRow[];
|
|
180
|
+
return rows.map(rowToProject);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface UpdateProjectInput {
|
|
184
|
+
name?: string;
|
|
185
|
+
description?: string;
|
|
186
|
+
client?: string;
|
|
187
|
+
status?: string;
|
|
188
|
+
budget?: number;
|
|
189
|
+
spent?: number;
|
|
190
|
+
currency?: string;
|
|
191
|
+
start_date?: string;
|
|
192
|
+
end_date?: string;
|
|
193
|
+
owner?: string;
|
|
194
|
+
tags?: string[];
|
|
195
|
+
metadata?: Record<string, unknown>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function updateProject(
|
|
199
|
+
id: string,
|
|
200
|
+
input: UpdateProjectInput
|
|
201
|
+
): Project | null {
|
|
202
|
+
const db = getDatabase();
|
|
203
|
+
const existing = getProject(id);
|
|
204
|
+
if (!existing) return null;
|
|
205
|
+
|
|
206
|
+
const sets: string[] = [];
|
|
207
|
+
const params: unknown[] = [];
|
|
208
|
+
|
|
209
|
+
if (input.name !== undefined) {
|
|
210
|
+
sets.push("name = ?");
|
|
211
|
+
params.push(input.name);
|
|
212
|
+
}
|
|
213
|
+
if (input.description !== undefined) {
|
|
214
|
+
sets.push("description = ?");
|
|
215
|
+
params.push(input.description);
|
|
216
|
+
}
|
|
217
|
+
if (input.client !== undefined) {
|
|
218
|
+
sets.push("client = ?");
|
|
219
|
+
params.push(input.client);
|
|
220
|
+
}
|
|
221
|
+
if (input.status !== undefined) {
|
|
222
|
+
sets.push("status = ?");
|
|
223
|
+
params.push(input.status);
|
|
224
|
+
}
|
|
225
|
+
if (input.budget !== undefined) {
|
|
226
|
+
sets.push("budget = ?");
|
|
227
|
+
params.push(input.budget);
|
|
228
|
+
}
|
|
229
|
+
if (input.spent !== undefined) {
|
|
230
|
+
sets.push("spent = ?");
|
|
231
|
+
params.push(input.spent);
|
|
232
|
+
}
|
|
233
|
+
if (input.currency !== undefined) {
|
|
234
|
+
sets.push("currency = ?");
|
|
235
|
+
params.push(input.currency);
|
|
236
|
+
}
|
|
237
|
+
if (input.start_date !== undefined) {
|
|
238
|
+
sets.push("start_date = ?");
|
|
239
|
+
params.push(input.start_date);
|
|
240
|
+
}
|
|
241
|
+
if (input.end_date !== undefined) {
|
|
242
|
+
sets.push("end_date = ?");
|
|
243
|
+
params.push(input.end_date);
|
|
244
|
+
}
|
|
245
|
+
if (input.owner !== undefined) {
|
|
246
|
+
sets.push("owner = ?");
|
|
247
|
+
params.push(input.owner);
|
|
248
|
+
}
|
|
249
|
+
if (input.tags !== undefined) {
|
|
250
|
+
sets.push("tags = ?");
|
|
251
|
+
params.push(JSON.stringify(input.tags));
|
|
252
|
+
}
|
|
253
|
+
if (input.metadata !== undefined) {
|
|
254
|
+
sets.push("metadata = ?");
|
|
255
|
+
params.push(JSON.stringify(input.metadata));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (sets.length === 0) return existing;
|
|
259
|
+
|
|
260
|
+
sets.push("updated_at = datetime('now')");
|
|
261
|
+
params.push(id);
|
|
262
|
+
|
|
263
|
+
db.prepare(
|
|
264
|
+
`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`
|
|
265
|
+
).run(...params);
|
|
266
|
+
|
|
267
|
+
return getProject(id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function deleteProject(id: string): boolean {
|
|
271
|
+
const db = getDatabase();
|
|
272
|
+
const result = db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
273
|
+
return result.changes > 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function searchProjects(query: string): Project[] {
|
|
277
|
+
return listProjects({ search: query });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- Milestone CRUD ---
|
|
281
|
+
|
|
282
|
+
export interface CreateMilestoneInput {
|
|
283
|
+
project_id: string;
|
|
284
|
+
name: string;
|
|
285
|
+
description?: string;
|
|
286
|
+
due_date?: string;
|
|
287
|
+
status?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createMilestone(input: CreateMilestoneInput): Milestone {
|
|
291
|
+
const db = getDatabase();
|
|
292
|
+
const id = crypto.randomUUID();
|
|
293
|
+
|
|
294
|
+
db.prepare(
|
|
295
|
+
`INSERT INTO milestones (id, project_id, name, description, due_date, status)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
297
|
+
).run(
|
|
298
|
+
id,
|
|
299
|
+
input.project_id,
|
|
300
|
+
input.name,
|
|
301
|
+
input.description || null,
|
|
302
|
+
input.due_date || null,
|
|
303
|
+
input.status || "pending"
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
return getMilestone(id)!;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function getMilestone(id: string): Milestone | null {
|
|
310
|
+
const db = getDatabase();
|
|
311
|
+
return db.prepare("SELECT * FROM milestones WHERE id = ?").get(id) as Milestone | null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface ListMilestonesOptions {
|
|
315
|
+
project_id?: string;
|
|
316
|
+
status?: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function listMilestones(options: ListMilestonesOptions = {}): Milestone[] {
|
|
320
|
+
const db = getDatabase();
|
|
321
|
+
const conditions: string[] = [];
|
|
322
|
+
const params: unknown[] = [];
|
|
323
|
+
|
|
324
|
+
if (options.project_id) {
|
|
325
|
+
conditions.push("project_id = ?");
|
|
326
|
+
params.push(options.project_id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (options.status) {
|
|
330
|
+
conditions.push("status = ?");
|
|
331
|
+
params.push(options.status);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let sql = "SELECT * FROM milestones";
|
|
335
|
+
if (conditions.length > 0) {
|
|
336
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
337
|
+
}
|
|
338
|
+
sql += " ORDER BY due_date ASC, created_at ASC";
|
|
339
|
+
|
|
340
|
+
const rows = db.prepare(sql).all(...params) as Milestone[];
|
|
341
|
+
return rows;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface UpdateMilestoneInput {
|
|
345
|
+
name?: string;
|
|
346
|
+
description?: string;
|
|
347
|
+
due_date?: string;
|
|
348
|
+
status?: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function updateMilestone(
|
|
352
|
+
id: string,
|
|
353
|
+
input: UpdateMilestoneInput
|
|
354
|
+
): Milestone | null {
|
|
355
|
+
const db = getDatabase();
|
|
356
|
+
const existing = getMilestone(id);
|
|
357
|
+
if (!existing) return null;
|
|
358
|
+
|
|
359
|
+
const sets: string[] = [];
|
|
360
|
+
const params: unknown[] = [];
|
|
361
|
+
|
|
362
|
+
if (input.name !== undefined) {
|
|
363
|
+
sets.push("name = ?");
|
|
364
|
+
params.push(input.name);
|
|
365
|
+
}
|
|
366
|
+
if (input.description !== undefined) {
|
|
367
|
+
sets.push("description = ?");
|
|
368
|
+
params.push(input.description);
|
|
369
|
+
}
|
|
370
|
+
if (input.due_date !== undefined) {
|
|
371
|
+
sets.push("due_date = ?");
|
|
372
|
+
params.push(input.due_date);
|
|
373
|
+
}
|
|
374
|
+
if (input.status !== undefined) {
|
|
375
|
+
sets.push("status = ?");
|
|
376
|
+
params.push(input.status);
|
|
377
|
+
if (input.status === "completed") {
|
|
378
|
+
sets.push("completed_at = datetime('now')");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (sets.length === 0) return existing;
|
|
383
|
+
params.push(id);
|
|
384
|
+
|
|
385
|
+
db.prepare(
|
|
386
|
+
`UPDATE milestones SET ${sets.join(", ")} WHERE id = ?`
|
|
387
|
+
).run(...params);
|
|
388
|
+
|
|
389
|
+
return getMilestone(id);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function completeMilestone(id: string): Milestone | null {
|
|
393
|
+
return updateMilestone(id, { status: "completed" });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function deleteMilestone(id: string): boolean {
|
|
397
|
+
const db = getDatabase();
|
|
398
|
+
const result = db.prepare("DELETE FROM milestones WHERE id = ?").run(id);
|
|
399
|
+
return result.changes > 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Deliverable CRUD ---
|
|
403
|
+
|
|
404
|
+
export interface CreateDeliverableInput {
|
|
405
|
+
milestone_id: string;
|
|
406
|
+
name: string;
|
|
407
|
+
description?: string;
|
|
408
|
+
status?: string;
|
|
409
|
+
assignee?: string;
|
|
410
|
+
due_date?: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function createDeliverable(input: CreateDeliverableInput): Deliverable {
|
|
414
|
+
const db = getDatabase();
|
|
415
|
+
const id = crypto.randomUUID();
|
|
416
|
+
|
|
417
|
+
db.prepare(
|
|
418
|
+
`INSERT INTO deliverables (id, milestone_id, name, description, status, assignee, due_date)
|
|
419
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
420
|
+
).run(
|
|
421
|
+
id,
|
|
422
|
+
input.milestone_id,
|
|
423
|
+
input.name,
|
|
424
|
+
input.description || null,
|
|
425
|
+
input.status || "pending",
|
|
426
|
+
input.assignee || null,
|
|
427
|
+
input.due_date || null
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
return getDeliverable(id)!;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function getDeliverable(id: string): Deliverable | null {
|
|
434
|
+
const db = getDatabase();
|
|
435
|
+
return db.prepare("SELECT * FROM deliverables WHERE id = ?").get(id) as Deliverable | null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface ListDeliverablesOptions {
|
|
439
|
+
milestone_id?: string;
|
|
440
|
+
status?: string;
|
|
441
|
+
assignee?: string;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function listDeliverables(options: ListDeliverablesOptions = {}): Deliverable[] {
|
|
445
|
+
const db = getDatabase();
|
|
446
|
+
const conditions: string[] = [];
|
|
447
|
+
const params: unknown[] = [];
|
|
448
|
+
|
|
449
|
+
if (options.milestone_id) {
|
|
450
|
+
conditions.push("milestone_id = ?");
|
|
451
|
+
params.push(options.milestone_id);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (options.status) {
|
|
455
|
+
conditions.push("status = ?");
|
|
456
|
+
params.push(options.status);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (options.assignee) {
|
|
460
|
+
conditions.push("assignee = ?");
|
|
461
|
+
params.push(options.assignee);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let sql = "SELECT * FROM deliverables";
|
|
465
|
+
if (conditions.length > 0) {
|
|
466
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
467
|
+
}
|
|
468
|
+
sql += " ORDER BY due_date ASC, created_at ASC";
|
|
469
|
+
|
|
470
|
+
const rows = db.prepare(sql).all(...params) as Deliverable[];
|
|
471
|
+
return rows;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface UpdateDeliverableInput {
|
|
475
|
+
name?: string;
|
|
476
|
+
description?: string;
|
|
477
|
+
status?: string;
|
|
478
|
+
assignee?: string;
|
|
479
|
+
due_date?: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function updateDeliverable(
|
|
483
|
+
id: string,
|
|
484
|
+
input: UpdateDeliverableInput
|
|
485
|
+
): Deliverable | null {
|
|
486
|
+
const db = getDatabase();
|
|
487
|
+
const existing = getDeliverable(id);
|
|
488
|
+
if (!existing) return null;
|
|
489
|
+
|
|
490
|
+
const sets: string[] = [];
|
|
491
|
+
const params: unknown[] = [];
|
|
492
|
+
|
|
493
|
+
if (input.name !== undefined) {
|
|
494
|
+
sets.push("name = ?");
|
|
495
|
+
params.push(input.name);
|
|
496
|
+
}
|
|
497
|
+
if (input.description !== undefined) {
|
|
498
|
+
sets.push("description = ?");
|
|
499
|
+
params.push(input.description);
|
|
500
|
+
}
|
|
501
|
+
if (input.status !== undefined) {
|
|
502
|
+
sets.push("status = ?");
|
|
503
|
+
params.push(input.status);
|
|
504
|
+
if (input.status === "completed") {
|
|
505
|
+
sets.push("completed_at = datetime('now')");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (input.assignee !== undefined) {
|
|
509
|
+
sets.push("assignee = ?");
|
|
510
|
+
params.push(input.assignee);
|
|
511
|
+
}
|
|
512
|
+
if (input.due_date !== undefined) {
|
|
513
|
+
sets.push("due_date = ?");
|
|
514
|
+
params.push(input.due_date);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (sets.length === 0) return existing;
|
|
518
|
+
params.push(id);
|
|
519
|
+
|
|
520
|
+
db.prepare(
|
|
521
|
+
`UPDATE deliverables SET ${sets.join(", ")} WHERE id = ?`
|
|
522
|
+
).run(...params);
|
|
523
|
+
|
|
524
|
+
return getDeliverable(id);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function completeDeliverable(id: string): Deliverable | null {
|
|
528
|
+
return updateDeliverable(id, { status: "completed" });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function deleteDeliverable(id: string): boolean {
|
|
532
|
+
const db = getDatabase();
|
|
533
|
+
const result = db.prepare("DELETE FROM deliverables WHERE id = ?").run(id);
|
|
534
|
+
return result.changes > 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// --- Advanced Queries ---
|
|
538
|
+
|
|
539
|
+
export interface TimelineEntry {
|
|
540
|
+
type: "milestone" | "deliverable";
|
|
541
|
+
id: string;
|
|
542
|
+
name: string;
|
|
543
|
+
status: string;
|
|
544
|
+
due_date: string | null;
|
|
545
|
+
parent_id?: string;
|
|
546
|
+
parent_name?: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function getProjectTimeline(projectId: string): TimelineEntry[] {
|
|
550
|
+
const db = getDatabase();
|
|
551
|
+
|
|
552
|
+
const milestones = db.prepare(
|
|
553
|
+
"SELECT * FROM milestones WHERE project_id = ? ORDER BY due_date ASC, created_at ASC"
|
|
554
|
+
).all(projectId) as Milestone[];
|
|
555
|
+
|
|
556
|
+
const entries: TimelineEntry[] = [];
|
|
557
|
+
|
|
558
|
+
for (const m of milestones) {
|
|
559
|
+
entries.push({
|
|
560
|
+
type: "milestone",
|
|
561
|
+
id: m.id,
|
|
562
|
+
name: m.name,
|
|
563
|
+
status: m.status,
|
|
564
|
+
due_date: m.due_date,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const deliverables = db.prepare(
|
|
568
|
+
"SELECT * FROM deliverables WHERE milestone_id = ? ORDER BY due_date ASC, created_at ASC"
|
|
569
|
+
).all(m.id) as Deliverable[];
|
|
570
|
+
|
|
571
|
+
for (const d of deliverables) {
|
|
572
|
+
entries.push({
|
|
573
|
+
type: "deliverable",
|
|
574
|
+
id: d.id,
|
|
575
|
+
name: d.name,
|
|
576
|
+
status: d.status,
|
|
577
|
+
due_date: d.due_date,
|
|
578
|
+
parent_id: m.id,
|
|
579
|
+
parent_name: m.name,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return entries;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export interface BudgetReport {
|
|
588
|
+
project_id: string;
|
|
589
|
+
project_name: string;
|
|
590
|
+
budget: number | null;
|
|
591
|
+
spent: number;
|
|
592
|
+
remaining: number | null;
|
|
593
|
+
currency: string;
|
|
594
|
+
utilization_pct: number | null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function getBudgetVsActual(projectId: string): BudgetReport | null {
|
|
598
|
+
const project = getProject(projectId);
|
|
599
|
+
if (!project) return null;
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
project_id: project.id,
|
|
603
|
+
project_name: project.name,
|
|
604
|
+
budget: project.budget,
|
|
605
|
+
spent: project.spent,
|
|
606
|
+
remaining: project.budget !== null ? project.budget - project.spent : null,
|
|
607
|
+
currency: project.currency,
|
|
608
|
+
utilization_pct:
|
|
609
|
+
project.budget !== null && project.budget > 0
|
|
610
|
+
? Math.round((project.spent / project.budget) * 10000) / 100
|
|
611
|
+
: null,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function getOverdueProjects(): Project[] {
|
|
616
|
+
const db = getDatabase();
|
|
617
|
+
const rows = db.prepare(
|
|
618
|
+
`SELECT * FROM projects
|
|
619
|
+
WHERE end_date IS NOT NULL
|
|
620
|
+
AND end_date < datetime('now')
|
|
621
|
+
AND status NOT IN ('completed', 'cancelled')
|
|
622
|
+
ORDER BY end_date ASC`
|
|
623
|
+
).all() as ProjectRow[];
|
|
624
|
+
return rows.map(rowToProject);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function getOverdueMilestones(): Milestone[] {
|
|
628
|
+
const db = getDatabase();
|
|
629
|
+
return db.prepare(
|
|
630
|
+
`SELECT * FROM milestones
|
|
631
|
+
WHERE due_date IS NOT NULL
|
|
632
|
+
AND due_date < datetime('now')
|
|
633
|
+
AND status NOT IN ('completed', 'missed')
|
|
634
|
+
ORDER BY due_date ASC`
|
|
635
|
+
).all() as Milestone[];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export interface ProjectStats {
|
|
639
|
+
total: number;
|
|
640
|
+
by_status: Record<string, number>;
|
|
641
|
+
total_budget: number;
|
|
642
|
+
total_spent: number;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function getProjectStats(): ProjectStats {
|
|
646
|
+
const db = getDatabase();
|
|
647
|
+
|
|
648
|
+
const total = (
|
|
649
|
+
db.prepare("SELECT COUNT(*) as count FROM projects").get() as { count: number }
|
|
650
|
+
).count;
|
|
651
|
+
|
|
652
|
+
const statusRows = db
|
|
653
|
+
.prepare("SELECT status, COUNT(*) as count FROM projects GROUP BY status")
|
|
654
|
+
.all() as { status: string; count: number }[];
|
|
655
|
+
|
|
656
|
+
const by_status: Record<string, number> = {};
|
|
657
|
+
for (const row of statusRows) {
|
|
658
|
+
by_status[row.status] = row.count;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const budgetRow = db.prepare(
|
|
662
|
+
"SELECT COALESCE(SUM(budget), 0) as total_budget, COALESCE(SUM(spent), 0) as total_spent FROM projects"
|
|
663
|
+
).get() as { total_budget: number; total_spent: number };
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
total,
|
|
667
|
+
by_status,
|
|
668
|
+
total_budget: budgetRow.total_budget,
|
|
669
|
+
total_spent: budgetRow.total_spent,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export interface MilestoneProgress {
|
|
674
|
+
project_id: string;
|
|
675
|
+
total: number;
|
|
676
|
+
completed: number;
|
|
677
|
+
in_progress: number;
|
|
678
|
+
pending: number;
|
|
679
|
+
missed: number;
|
|
680
|
+
completion_pct: number;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function getMilestoneProgress(projectId: string): MilestoneProgress {
|
|
684
|
+
const db = getDatabase();
|
|
685
|
+
|
|
686
|
+
const rows = db
|
|
687
|
+
.prepare(
|
|
688
|
+
"SELECT status, COUNT(*) as count FROM milestones WHERE project_id = ? GROUP BY status"
|
|
689
|
+
)
|
|
690
|
+
.all(projectId) as { status: string; count: number }[];
|
|
691
|
+
|
|
692
|
+
let total = 0;
|
|
693
|
+
let completed = 0;
|
|
694
|
+
let in_progress = 0;
|
|
695
|
+
let pending = 0;
|
|
696
|
+
let missed = 0;
|
|
697
|
+
|
|
698
|
+
for (const row of rows) {
|
|
699
|
+
total += row.count;
|
|
700
|
+
if (row.status === "completed") completed = row.count;
|
|
701
|
+
else if (row.status === "in_progress") in_progress = row.count;
|
|
702
|
+
else if (row.status === "pending") pending = row.count;
|
|
703
|
+
else if (row.status === "missed") missed = row.count;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
project_id: projectId,
|
|
708
|
+
total,
|
|
709
|
+
completed,
|
|
710
|
+
in_progress,
|
|
711
|
+
pending,
|
|
712
|
+
missed,
|
|
713
|
+
completion_pct: total > 0 ? Math.round((completed / total) * 10000) / 100 : 0,
|
|
714
|
+
};
|
|
715
|
+
}
|