@hasna/microservices 0.0.3 → 0.0.4
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 +63 -0
- package/bin/mcp.js +63 -0
- package/dist/index.js +63 -0
- package/microservices/microservice-ads/package.json +27 -0
- package/microservices/microservice-ads/src/cli/index.ts +407 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +493 -0
- package/microservices/microservice-ads/src/db/database.ts +93 -0
- package/microservices/microservice-ads/src/db/migrations.ts +60 -0
- package/microservices/microservice-ads/src/index.ts +39 -0
- package/microservices/microservice-ads/src/mcp/index.ts +320 -0
- package/microservices/microservice-contracts/package.json +27 -0
- package/microservices/microservice-contracts/src/cli/index.ts +383 -0
- package/microservices/microservice-contracts/src/db/contracts.ts +496 -0
- package/microservices/microservice-contracts/src/db/database.ts +93 -0
- package/microservices/microservice-contracts/src/db/migrations.ts +58 -0
- package/microservices/microservice-contracts/src/index.ts +43 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +308 -0
- package/microservices/microservice-domains/package.json +27 -0
- package/microservices/microservice-domains/src/cli/index.ts +438 -0
- package/microservices/microservice-domains/src/db/database.ts +93 -0
- package/microservices/microservice-domains/src/db/domains.ts +551 -0
- package/microservices/microservice-domains/src/db/migrations.ts +60 -0
- package/microservices/microservice-domains/src/index.ts +44 -0
- package/microservices/microservice-domains/src/mcp/index.ts +368 -0
- package/microservices/microservice-hiring/package.json +27 -0
- package/microservices/microservice-hiring/src/cli/index.ts +431 -0
- package/microservices/microservice-hiring/src/db/database.ts +93 -0
- package/microservices/microservice-hiring/src/db/hiring.ts +582 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +68 -0
- package/microservices/microservice-hiring/src/index.ts +51 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +464 -0
- package/microservices/microservice-payments/package.json +27 -0
- package/microservices/microservice-payments/src/cli/index.ts +357 -0
- package/microservices/microservice-payments/src/db/database.ts +93 -0
- package/microservices/microservice-payments/src/db/migrations.ts +63 -0
- package/microservices/microservice-payments/src/db/payments.ts +652 -0
- package/microservices/microservice-payments/src/index.ts +51 -0
- package/microservices/microservice-payments/src/mcp/index.ts +460 -0
- package/microservices/microservice-payroll/package.json +27 -0
- package/microservices/microservice-payroll/src/cli/index.ts +374 -0
- package/microservices/microservice-payroll/src/db/database.ts +93 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +69 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +741 -0
- package/microservices/microservice-payroll/src/index.ts +48 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +420 -0
- package/microservices/microservice-shipping/package.json +27 -0
- package/microservices/microservice-shipping/src/cli/index.ts +398 -0
- package/microservices/microservice-shipping/src/db/database.ts +93 -0
- package/microservices/microservice-shipping/src/db/migrations.ts +61 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +643 -0
- package/microservices/microservice-shipping/src/index.ts +53 -0
- package/microservices/microservice-shipping/src/mcp/index.ts +385 -0
- package/microservices/microservice-social/package.json +27 -0
- package/microservices/microservice-social/src/cli/index.ts +447 -0
- package/microservices/microservice-social/src/db/database.ts +93 -0
- package/microservices/microservice-social/src/db/migrations.ts +55 -0
- package/microservices/microservice-social/src/db/social.ts +672 -0
- package/microservices/microservice-social/src/index.ts +46 -0
- package/microservices/microservice-social/src/mcp/index.ts +435 -0
- package/microservices/microservice-subscriptions/package.json +27 -0
- package/microservices/microservice-subscriptions/src/cli/index.ts +400 -0
- package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +57 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +692 -0
- package/microservices/microservice-subscriptions/src/index.ts +41 -0
- package/microservices/microservice-subscriptions/src/mcp/index.ts +365 -0
- package/package.json +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hiring CRUD operations — jobs, applicants, interviews
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// ---- Types ----
|
|
8
|
+
|
|
9
|
+
export interface Job {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
department: string | null;
|
|
13
|
+
location: string | null;
|
|
14
|
+
type: "full-time" | "part-time" | "contract";
|
|
15
|
+
status: "open" | "closed" | "paused";
|
|
16
|
+
description: string | null;
|
|
17
|
+
requirements: string[];
|
|
18
|
+
salary_range: string | null;
|
|
19
|
+
posted_at: string | null;
|
|
20
|
+
closed_at: string | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface JobRow {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
department: string | null;
|
|
29
|
+
location: string | null;
|
|
30
|
+
type: string;
|
|
31
|
+
status: string;
|
|
32
|
+
description: string | null;
|
|
33
|
+
requirements: string;
|
|
34
|
+
salary_range: string | null;
|
|
35
|
+
posted_at: string | null;
|
|
36
|
+
closed_at: string | null;
|
|
37
|
+
created_at: string;
|
|
38
|
+
updated_at: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Applicant {
|
|
42
|
+
id: string;
|
|
43
|
+
job_id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
email: string | null;
|
|
46
|
+
phone: string | null;
|
|
47
|
+
resume_url: string | null;
|
|
48
|
+
status: "applied" | "screening" | "interviewing" | "offered" | "hired" | "rejected";
|
|
49
|
+
stage: string | null;
|
|
50
|
+
rating: number | null;
|
|
51
|
+
notes: string | null;
|
|
52
|
+
source: string | null;
|
|
53
|
+
applied_at: string;
|
|
54
|
+
metadata: Record<string, unknown>;
|
|
55
|
+
created_at: string;
|
|
56
|
+
updated_at: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ApplicantRow {
|
|
60
|
+
id: string;
|
|
61
|
+
job_id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
email: string | null;
|
|
64
|
+
phone: string | null;
|
|
65
|
+
resume_url: string | null;
|
|
66
|
+
status: string;
|
|
67
|
+
stage: string | null;
|
|
68
|
+
rating: number | null;
|
|
69
|
+
notes: string | null;
|
|
70
|
+
source: string | null;
|
|
71
|
+
applied_at: string;
|
|
72
|
+
metadata: string;
|
|
73
|
+
created_at: string;
|
|
74
|
+
updated_at: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Interview {
|
|
78
|
+
id: string;
|
|
79
|
+
applicant_id: string;
|
|
80
|
+
interviewer: string | null;
|
|
81
|
+
scheduled_at: string | null;
|
|
82
|
+
duration_min: number | null;
|
|
83
|
+
type: "phone" | "video" | "onsite";
|
|
84
|
+
status: "scheduled" | "completed" | "canceled";
|
|
85
|
+
feedback: string | null;
|
|
86
|
+
rating: number | null;
|
|
87
|
+
created_at: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Row converters ----
|
|
91
|
+
|
|
92
|
+
function rowToJob(row: JobRow): Job {
|
|
93
|
+
return {
|
|
94
|
+
...row,
|
|
95
|
+
type: row.type as Job["type"],
|
|
96
|
+
status: row.status as Job["status"],
|
|
97
|
+
requirements: JSON.parse(row.requirements || "[]"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rowToApplicant(row: ApplicantRow): Applicant {
|
|
102
|
+
return {
|
|
103
|
+
...row,
|
|
104
|
+
status: row.status as Applicant["status"],
|
|
105
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Jobs ----
|
|
110
|
+
|
|
111
|
+
export interface CreateJobInput {
|
|
112
|
+
title: string;
|
|
113
|
+
department?: string;
|
|
114
|
+
location?: string;
|
|
115
|
+
type?: "full-time" | "part-time" | "contract";
|
|
116
|
+
description?: string;
|
|
117
|
+
requirements?: string[];
|
|
118
|
+
salary_range?: string;
|
|
119
|
+
posted_at?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createJob(input: CreateJobInput): Job {
|
|
123
|
+
const db = getDatabase();
|
|
124
|
+
const id = crypto.randomUUID();
|
|
125
|
+
const requirements = JSON.stringify(input.requirements || []);
|
|
126
|
+
|
|
127
|
+
db.prepare(
|
|
128
|
+
`INSERT INTO jobs (id, title, department, location, type, description, requirements, salary_range, posted_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
130
|
+
).run(
|
|
131
|
+
id,
|
|
132
|
+
input.title,
|
|
133
|
+
input.department || null,
|
|
134
|
+
input.location || null,
|
|
135
|
+
input.type || "full-time",
|
|
136
|
+
input.description || null,
|
|
137
|
+
requirements,
|
|
138
|
+
input.salary_range || null,
|
|
139
|
+
input.posted_at || null
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return getJob(id)!;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getJob(id: string): Job | null {
|
|
146
|
+
const db = getDatabase();
|
|
147
|
+
const row = db.prepare("SELECT * FROM jobs WHERE id = ?").get(id) as JobRow | null;
|
|
148
|
+
return row ? rowToJob(row) : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ListJobsOptions {
|
|
152
|
+
status?: string;
|
|
153
|
+
department?: string;
|
|
154
|
+
type?: string;
|
|
155
|
+
limit?: number;
|
|
156
|
+
offset?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function listJobs(options: ListJobsOptions = {}): Job[] {
|
|
160
|
+
const db = getDatabase();
|
|
161
|
+
const conditions: string[] = [];
|
|
162
|
+
const params: unknown[] = [];
|
|
163
|
+
|
|
164
|
+
if (options.status) {
|
|
165
|
+
conditions.push("status = ?");
|
|
166
|
+
params.push(options.status);
|
|
167
|
+
}
|
|
168
|
+
if (options.department) {
|
|
169
|
+
conditions.push("department = ?");
|
|
170
|
+
params.push(options.department);
|
|
171
|
+
}
|
|
172
|
+
if (options.type) {
|
|
173
|
+
conditions.push("type = ?");
|
|
174
|
+
params.push(options.type);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let sql = "SELECT * FROM jobs";
|
|
178
|
+
if (conditions.length > 0) {
|
|
179
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
180
|
+
}
|
|
181
|
+
sql += " ORDER BY created_at DESC";
|
|
182
|
+
|
|
183
|
+
if (options.limit) {
|
|
184
|
+
sql += " LIMIT ?";
|
|
185
|
+
params.push(options.limit);
|
|
186
|
+
}
|
|
187
|
+
if (options.offset) {
|
|
188
|
+
sql += " OFFSET ?";
|
|
189
|
+
params.push(options.offset);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const rows = db.prepare(sql).all(...params) as JobRow[];
|
|
193
|
+
return rows.map(rowToJob);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface UpdateJobInput {
|
|
197
|
+
title?: string;
|
|
198
|
+
department?: string;
|
|
199
|
+
location?: string;
|
|
200
|
+
type?: "full-time" | "part-time" | "contract";
|
|
201
|
+
status?: "open" | "closed" | "paused";
|
|
202
|
+
description?: string;
|
|
203
|
+
requirements?: string[];
|
|
204
|
+
salary_range?: string;
|
|
205
|
+
posted_at?: string;
|
|
206
|
+
closed_at?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function updateJob(id: string, input: UpdateJobInput): Job | null {
|
|
210
|
+
const db = getDatabase();
|
|
211
|
+
const existing = getJob(id);
|
|
212
|
+
if (!existing) return null;
|
|
213
|
+
|
|
214
|
+
const sets: string[] = [];
|
|
215
|
+
const params: unknown[] = [];
|
|
216
|
+
|
|
217
|
+
if (input.title !== undefined) { sets.push("title = ?"); params.push(input.title); }
|
|
218
|
+
if (input.department !== undefined) { sets.push("department = ?"); params.push(input.department); }
|
|
219
|
+
if (input.location !== undefined) { sets.push("location = ?"); params.push(input.location); }
|
|
220
|
+
if (input.type !== undefined) { sets.push("type = ?"); params.push(input.type); }
|
|
221
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
222
|
+
if (input.description !== undefined) { sets.push("description = ?"); params.push(input.description); }
|
|
223
|
+
if (input.requirements !== undefined) { sets.push("requirements = ?"); params.push(JSON.stringify(input.requirements)); }
|
|
224
|
+
if (input.salary_range !== undefined) { sets.push("salary_range = ?"); params.push(input.salary_range); }
|
|
225
|
+
if (input.posted_at !== undefined) { sets.push("posted_at = ?"); params.push(input.posted_at); }
|
|
226
|
+
if (input.closed_at !== undefined) { sets.push("closed_at = ?"); params.push(input.closed_at); }
|
|
227
|
+
|
|
228
|
+
if (sets.length === 0) return existing;
|
|
229
|
+
|
|
230
|
+
sets.push("updated_at = datetime('now')");
|
|
231
|
+
params.push(id);
|
|
232
|
+
|
|
233
|
+
db.prepare(`UPDATE jobs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
234
|
+
return getJob(id);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function closeJob(id: string): Job | null {
|
|
238
|
+
return updateJob(id, { status: "closed", closed_at: new Date().toISOString() });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function deleteJob(id: string): boolean {
|
|
242
|
+
const db = getDatabase();
|
|
243
|
+
const result = db.prepare("DELETE FROM jobs WHERE id = ?").run(id);
|
|
244
|
+
return result.changes > 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Applicants ----
|
|
248
|
+
|
|
249
|
+
export interface CreateApplicantInput {
|
|
250
|
+
job_id: string;
|
|
251
|
+
name: string;
|
|
252
|
+
email?: string;
|
|
253
|
+
phone?: string;
|
|
254
|
+
resume_url?: string;
|
|
255
|
+
status?: Applicant["status"];
|
|
256
|
+
stage?: string;
|
|
257
|
+
rating?: number;
|
|
258
|
+
notes?: string;
|
|
259
|
+
source?: string;
|
|
260
|
+
metadata?: Record<string, unknown>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function createApplicant(input: CreateApplicantInput): Applicant {
|
|
264
|
+
const db = getDatabase();
|
|
265
|
+
const id = crypto.randomUUID();
|
|
266
|
+
const metadata = JSON.stringify(input.metadata || {});
|
|
267
|
+
|
|
268
|
+
db.prepare(
|
|
269
|
+
`INSERT INTO applicants (id, job_id, name, email, phone, resume_url, status, stage, rating, notes, source, metadata)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
271
|
+
).run(
|
|
272
|
+
id,
|
|
273
|
+
input.job_id,
|
|
274
|
+
input.name,
|
|
275
|
+
input.email || null,
|
|
276
|
+
input.phone || null,
|
|
277
|
+
input.resume_url || null,
|
|
278
|
+
input.status || "applied",
|
|
279
|
+
input.stage || null,
|
|
280
|
+
input.rating ?? null,
|
|
281
|
+
input.notes || null,
|
|
282
|
+
input.source || null,
|
|
283
|
+
metadata
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return getApplicant(id)!;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getApplicant(id: string): Applicant | null {
|
|
290
|
+
const db = getDatabase();
|
|
291
|
+
const row = db.prepare("SELECT * FROM applicants WHERE id = ?").get(id) as ApplicantRow | null;
|
|
292
|
+
return row ? rowToApplicant(row) : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface ListApplicantsOptions {
|
|
296
|
+
job_id?: string;
|
|
297
|
+
status?: string;
|
|
298
|
+
source?: string;
|
|
299
|
+
limit?: number;
|
|
300
|
+
offset?: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function listApplicants(options: ListApplicantsOptions = {}): Applicant[] {
|
|
304
|
+
const db = getDatabase();
|
|
305
|
+
const conditions: string[] = [];
|
|
306
|
+
const params: unknown[] = [];
|
|
307
|
+
|
|
308
|
+
if (options.job_id) {
|
|
309
|
+
conditions.push("job_id = ?");
|
|
310
|
+
params.push(options.job_id);
|
|
311
|
+
}
|
|
312
|
+
if (options.status) {
|
|
313
|
+
conditions.push("status = ?");
|
|
314
|
+
params.push(options.status);
|
|
315
|
+
}
|
|
316
|
+
if (options.source) {
|
|
317
|
+
conditions.push("source = ?");
|
|
318
|
+
params.push(options.source);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let sql = "SELECT * FROM applicants";
|
|
322
|
+
if (conditions.length > 0) {
|
|
323
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
324
|
+
}
|
|
325
|
+
sql += " ORDER BY applied_at DESC";
|
|
326
|
+
|
|
327
|
+
if (options.limit) {
|
|
328
|
+
sql += " LIMIT ?";
|
|
329
|
+
params.push(options.limit);
|
|
330
|
+
}
|
|
331
|
+
if (options.offset) {
|
|
332
|
+
sql += " OFFSET ?";
|
|
333
|
+
params.push(options.offset);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const rows = db.prepare(sql).all(...params) as ApplicantRow[];
|
|
337
|
+
return rows.map(rowToApplicant);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface UpdateApplicantInput {
|
|
341
|
+
name?: string;
|
|
342
|
+
email?: string;
|
|
343
|
+
phone?: string;
|
|
344
|
+
resume_url?: string;
|
|
345
|
+
status?: Applicant["status"];
|
|
346
|
+
stage?: string;
|
|
347
|
+
rating?: number;
|
|
348
|
+
notes?: string;
|
|
349
|
+
source?: string;
|
|
350
|
+
metadata?: Record<string, unknown>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function updateApplicant(id: string, input: UpdateApplicantInput): Applicant | null {
|
|
354
|
+
const db = getDatabase();
|
|
355
|
+
const existing = getApplicant(id);
|
|
356
|
+
if (!existing) return null;
|
|
357
|
+
|
|
358
|
+
const sets: string[] = [];
|
|
359
|
+
const params: unknown[] = [];
|
|
360
|
+
|
|
361
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
362
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
363
|
+
if (input.phone !== undefined) { sets.push("phone = ?"); params.push(input.phone); }
|
|
364
|
+
if (input.resume_url !== undefined) { sets.push("resume_url = ?"); params.push(input.resume_url); }
|
|
365
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
366
|
+
if (input.stage !== undefined) { sets.push("stage = ?"); params.push(input.stage); }
|
|
367
|
+
if (input.rating !== undefined) { sets.push("rating = ?"); params.push(input.rating); }
|
|
368
|
+
if (input.notes !== undefined) { sets.push("notes = ?"); params.push(input.notes); }
|
|
369
|
+
if (input.source !== undefined) { sets.push("source = ?"); params.push(input.source); }
|
|
370
|
+
if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
|
|
371
|
+
|
|
372
|
+
if (sets.length === 0) return existing;
|
|
373
|
+
|
|
374
|
+
sets.push("updated_at = datetime('now')");
|
|
375
|
+
params.push(id);
|
|
376
|
+
|
|
377
|
+
db.prepare(`UPDATE applicants SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
378
|
+
return getApplicant(id);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function advanceApplicant(id: string, newStatus: Applicant["status"]): Applicant | null {
|
|
382
|
+
return updateApplicant(id, { status: newStatus });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function rejectApplicant(id: string, reason?: string): Applicant | null {
|
|
386
|
+
const input: UpdateApplicantInput = { status: "rejected" };
|
|
387
|
+
if (reason) input.notes = reason;
|
|
388
|
+
return updateApplicant(id, input);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function deleteApplicant(id: string): boolean {
|
|
392
|
+
const db = getDatabase();
|
|
393
|
+
const result = db.prepare("DELETE FROM applicants WHERE id = ?").run(id);
|
|
394
|
+
return result.changes > 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function searchApplicants(query: string): Applicant[] {
|
|
398
|
+
const db = getDatabase();
|
|
399
|
+
const q = `%${query}%`;
|
|
400
|
+
const rows = db
|
|
401
|
+
.prepare(
|
|
402
|
+
"SELECT * FROM applicants WHERE name LIKE ? OR email LIKE ? OR notes LIKE ? OR source LIKE ? ORDER BY applied_at DESC"
|
|
403
|
+
)
|
|
404
|
+
.all(q, q, q, q) as ApplicantRow[];
|
|
405
|
+
return rows.map(rowToApplicant);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function listByStage(stage: string): Applicant[] {
|
|
409
|
+
const db = getDatabase();
|
|
410
|
+
const rows = db
|
|
411
|
+
.prepare("SELECT * FROM applicants WHERE stage = ? ORDER BY applied_at DESC")
|
|
412
|
+
.all(stage) as ApplicantRow[];
|
|
413
|
+
return rows.map(rowToApplicant);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface PipelineEntry {
|
|
417
|
+
status: string;
|
|
418
|
+
count: number;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function getPipeline(jobId: string): PipelineEntry[] {
|
|
422
|
+
const db = getDatabase();
|
|
423
|
+
const rows = db
|
|
424
|
+
.prepare(
|
|
425
|
+
"SELECT status, COUNT(*) as count FROM applicants WHERE job_id = ? GROUP BY status ORDER BY CASE status WHEN 'applied' THEN 1 WHEN 'screening' THEN 2 WHEN 'interviewing' THEN 3 WHEN 'offered' THEN 4 WHEN 'hired' THEN 5 WHEN 'rejected' THEN 6 END"
|
|
426
|
+
)
|
|
427
|
+
.all(jobId) as PipelineEntry[];
|
|
428
|
+
return rows;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface HiringStats {
|
|
432
|
+
total_jobs: number;
|
|
433
|
+
open_jobs: number;
|
|
434
|
+
total_applicants: number;
|
|
435
|
+
applicants_by_status: PipelineEntry[];
|
|
436
|
+
total_interviews: number;
|
|
437
|
+
avg_rating: number | null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function getHiringStats(): HiringStats {
|
|
441
|
+
const db = getDatabase();
|
|
442
|
+
|
|
443
|
+
const jobCount = db.prepare("SELECT COUNT(*) as count FROM jobs").get() as { count: number };
|
|
444
|
+
const openJobs = db.prepare("SELECT COUNT(*) as count FROM jobs WHERE status = 'open'").get() as { count: number };
|
|
445
|
+
const applicantCount = db.prepare("SELECT COUNT(*) as count FROM applicants").get() as { count: number };
|
|
446
|
+
const interviewCount = db.prepare("SELECT COUNT(*) as count FROM interviews").get() as { count: number };
|
|
447
|
+
const avgRating = db.prepare("SELECT AVG(rating) as avg FROM applicants WHERE rating IS NOT NULL").get() as { avg: number | null };
|
|
448
|
+
|
|
449
|
+
const byStatus = db
|
|
450
|
+
.prepare("SELECT status, COUNT(*) as count FROM applicants GROUP BY status")
|
|
451
|
+
.all() as PipelineEntry[];
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
total_jobs: jobCount.count,
|
|
455
|
+
open_jobs: openJobs.count,
|
|
456
|
+
total_applicants: applicantCount.count,
|
|
457
|
+
applicants_by_status: byStatus,
|
|
458
|
+
total_interviews: interviewCount.count,
|
|
459
|
+
avg_rating: avgRating.avg ? Math.round(avgRating.avg * 10) / 10 : null,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- Interviews ----
|
|
464
|
+
|
|
465
|
+
export interface CreateInterviewInput {
|
|
466
|
+
applicant_id: string;
|
|
467
|
+
interviewer?: string;
|
|
468
|
+
scheduled_at?: string;
|
|
469
|
+
duration_min?: number;
|
|
470
|
+
type?: "phone" | "video" | "onsite";
|
|
471
|
+
status?: "scheduled" | "completed" | "canceled";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function createInterview(input: CreateInterviewInput): Interview {
|
|
475
|
+
const db = getDatabase();
|
|
476
|
+
const id = crypto.randomUUID();
|
|
477
|
+
|
|
478
|
+
db.prepare(
|
|
479
|
+
`INSERT INTO interviews (id, applicant_id, interviewer, scheduled_at, duration_min, type, status)
|
|
480
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
481
|
+
).run(
|
|
482
|
+
id,
|
|
483
|
+
input.applicant_id,
|
|
484
|
+
input.interviewer || null,
|
|
485
|
+
input.scheduled_at || null,
|
|
486
|
+
input.duration_min ?? null,
|
|
487
|
+
input.type || "phone",
|
|
488
|
+
input.status || "scheduled"
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return getInterview(id)!;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function getInterview(id: string): Interview | null {
|
|
495
|
+
const db = getDatabase();
|
|
496
|
+
const row = db.prepare("SELECT * FROM interviews WHERE id = ?").get(id) as Interview | null;
|
|
497
|
+
return row || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export interface ListInterviewsOptions {
|
|
501
|
+
applicant_id?: string;
|
|
502
|
+
status?: string;
|
|
503
|
+
type?: string;
|
|
504
|
+
limit?: number;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function listInterviews(options: ListInterviewsOptions = {}): Interview[] {
|
|
508
|
+
const db = getDatabase();
|
|
509
|
+
const conditions: string[] = [];
|
|
510
|
+
const params: unknown[] = [];
|
|
511
|
+
|
|
512
|
+
if (options.applicant_id) {
|
|
513
|
+
conditions.push("applicant_id = ?");
|
|
514
|
+
params.push(options.applicant_id);
|
|
515
|
+
}
|
|
516
|
+
if (options.status) {
|
|
517
|
+
conditions.push("status = ?");
|
|
518
|
+
params.push(options.status);
|
|
519
|
+
}
|
|
520
|
+
if (options.type) {
|
|
521
|
+
conditions.push("type = ?");
|
|
522
|
+
params.push(options.type);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let sql = "SELECT * FROM interviews";
|
|
526
|
+
if (conditions.length > 0) {
|
|
527
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
528
|
+
}
|
|
529
|
+
sql += " ORDER BY scheduled_at DESC";
|
|
530
|
+
|
|
531
|
+
if (options.limit) {
|
|
532
|
+
sql += " LIMIT ?";
|
|
533
|
+
params.push(options.limit);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return db.prepare(sql).all(...params) as Interview[];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface UpdateInterviewInput {
|
|
540
|
+
interviewer?: string;
|
|
541
|
+
scheduled_at?: string;
|
|
542
|
+
duration_min?: number;
|
|
543
|
+
type?: "phone" | "video" | "onsite";
|
|
544
|
+
status?: "scheduled" | "completed" | "canceled";
|
|
545
|
+
feedback?: string;
|
|
546
|
+
rating?: number;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function updateInterview(id: string, input: UpdateInterviewInput): Interview | null {
|
|
550
|
+
const db = getDatabase();
|
|
551
|
+
const existing = getInterview(id);
|
|
552
|
+
if (!existing) return null;
|
|
553
|
+
|
|
554
|
+
const sets: string[] = [];
|
|
555
|
+
const params: unknown[] = [];
|
|
556
|
+
|
|
557
|
+
if (input.interviewer !== undefined) { sets.push("interviewer = ?"); params.push(input.interviewer); }
|
|
558
|
+
if (input.scheduled_at !== undefined) { sets.push("scheduled_at = ?"); params.push(input.scheduled_at); }
|
|
559
|
+
if (input.duration_min !== undefined) { sets.push("duration_min = ?"); params.push(input.duration_min); }
|
|
560
|
+
if (input.type !== undefined) { sets.push("type = ?"); params.push(input.type); }
|
|
561
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
562
|
+
if (input.feedback !== undefined) { sets.push("feedback = ?"); params.push(input.feedback); }
|
|
563
|
+
if (input.rating !== undefined) { sets.push("rating = ?"); params.push(input.rating); }
|
|
564
|
+
|
|
565
|
+
if (sets.length === 0) return existing;
|
|
566
|
+
|
|
567
|
+
params.push(id);
|
|
568
|
+
db.prepare(`UPDATE interviews SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
569
|
+
return getInterview(id);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function addInterviewFeedback(id: string, feedback: string, rating?: number): Interview | null {
|
|
573
|
+
const input: UpdateInterviewInput = { feedback, status: "completed" };
|
|
574
|
+
if (rating !== undefined) input.rating = rating;
|
|
575
|
+
return updateInterview(id, input);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function deleteInterview(id: string): boolean {
|
|
579
|
+
const db = getDatabase();
|
|
580
|
+
const result = db.prepare("DELETE FROM interviews WHERE id = ?").run(id);
|
|
581
|
+
return result.changes > 0;
|
|
582
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface MigrationEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MIGRATIONS: MigrationEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
name: "initial_schema",
|
|
11
|
+
sql: `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT NOT NULL,
|
|
15
|
+
department TEXT,
|
|
16
|
+
location TEXT,
|
|
17
|
+
type TEXT NOT NULL DEFAULT 'full-time' CHECK (type IN ('full-time', 'part-time', 'contract')),
|
|
18
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed', 'paused')),
|
|
19
|
+
description TEXT,
|
|
20
|
+
requirements TEXT NOT NULL DEFAULT '[]',
|
|
21
|
+
salary_range TEXT,
|
|
22
|
+
posted_at TEXT,
|
|
23
|
+
closed_at TEXT,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS applicants (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
31
|
+
name TEXT NOT NULL,
|
|
32
|
+
email TEXT,
|
|
33
|
+
phone TEXT,
|
|
34
|
+
resume_url TEXT,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'applied' CHECK (status IN ('applied', 'screening', 'interviewing', 'offered', 'hired', 'rejected')),
|
|
36
|
+
stage TEXT,
|
|
37
|
+
rating INTEGER,
|
|
38
|
+
notes TEXT,
|
|
39
|
+
source TEXT,
|
|
40
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
42
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS interviews (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
applicant_id TEXT NOT NULL REFERENCES applicants(id) ON DELETE CASCADE,
|
|
49
|
+
interviewer TEXT,
|
|
50
|
+
scheduled_at TEXT,
|
|
51
|
+
duration_min INTEGER,
|
|
52
|
+
type TEXT NOT NULL DEFAULT 'phone' CHECK (type IN ('phone', 'video', 'onsite')),
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'completed', 'canceled')),
|
|
54
|
+
feedback TEXT,
|
|
55
|
+
rating INTEGER,
|
|
56
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_department ON jobs(department);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_job_id ON applicants(job_id);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_status ON applicants(status);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_applicants_email ON applicants(email);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_interviews_applicant_id ON interviews(applicant_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_interviews_status ON interviews(status);
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* microservice-hiring — Applicant tracking and recruitment microservice
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createJob,
|
|
7
|
+
getJob,
|
|
8
|
+
listJobs,
|
|
9
|
+
updateJob,
|
|
10
|
+
closeJob,
|
|
11
|
+
deleteJob,
|
|
12
|
+
type Job,
|
|
13
|
+
type CreateJobInput,
|
|
14
|
+
type UpdateJobInput,
|
|
15
|
+
type ListJobsOptions,
|
|
16
|
+
} from "./db/hiring.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createApplicant,
|
|
20
|
+
getApplicant,
|
|
21
|
+
listApplicants,
|
|
22
|
+
updateApplicant,
|
|
23
|
+
advanceApplicant,
|
|
24
|
+
rejectApplicant,
|
|
25
|
+
deleteApplicant,
|
|
26
|
+
searchApplicants,
|
|
27
|
+
listByStage,
|
|
28
|
+
getPipeline,
|
|
29
|
+
getHiringStats,
|
|
30
|
+
type Applicant,
|
|
31
|
+
type CreateApplicantInput,
|
|
32
|
+
type UpdateApplicantInput,
|
|
33
|
+
type ListApplicantsOptions,
|
|
34
|
+
type PipelineEntry,
|
|
35
|
+
type HiringStats,
|
|
36
|
+
} from "./db/hiring.js";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
createInterview,
|
|
40
|
+
getInterview,
|
|
41
|
+
listInterviews,
|
|
42
|
+
updateInterview,
|
|
43
|
+
addInterviewFeedback,
|
|
44
|
+
deleteInterview,
|
|
45
|
+
type Interview,
|
|
46
|
+
type CreateInterviewInput,
|
|
47
|
+
type UpdateInterviewInput,
|
|
48
|
+
type ListInterviewsOptions,
|
|
49
|
+
} from "./db/hiring.js";
|
|
50
|
+
|
|
51
|
+
export { getDatabase, closeDatabase } from "./db/database.js";
|