domma-cms 0.7.7 → 0.7.8

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.
Files changed (31) hide show
  1. package/config/plugins.json +0 -8
  2. package/package.json +1 -1
  3. package/plugins/job-board/admin/templates/application-detail.html +0 -40
  4. package/plugins/job-board/admin/templates/applications.html +0 -10
  5. package/plugins/job-board/admin/templates/companies.html +0 -24
  6. package/plugins/job-board/admin/templates/dashboard.html +0 -36
  7. package/plugins/job-board/admin/templates/job-editor.html +0 -17
  8. package/plugins/job-board/admin/templates/jobs.html +0 -15
  9. package/plugins/job-board/admin/templates/profile.html +0 -17
  10. package/plugins/job-board/admin/views/application-detail.js +0 -62
  11. package/plugins/job-board/admin/views/applications.js +0 -47
  12. package/plugins/job-board/admin/views/companies.js +0 -104
  13. package/plugins/job-board/admin/views/dashboard.js +0 -88
  14. package/plugins/job-board/admin/views/job-editor.js +0 -86
  15. package/plugins/job-board/admin/views/jobs.js +0 -53
  16. package/plugins/job-board/admin/views/profile.js +0 -47
  17. package/plugins/job-board/config.js +0 -6
  18. package/plugins/job-board/plugin.js +0 -466
  19. package/plugins/job-board/plugin.json +0 -40
  20. package/plugins/job-board/schemas/jb-agent-companies.json +0 -17
  21. package/plugins/job-board/schemas/jb-applications.json +0 -20
  22. package/plugins/job-board/schemas/jb-candidate-profiles.json +0 -20
  23. package/plugins/job-board/schemas/jb-companies.json +0 -21
  24. package/plugins/job-board/schemas/jb-jobs.json +0 -23
  25. package/plugins/theme-roller/admin/templates/theme-roller.html +0 -71
  26. package/plugins/theme-roller/admin/views/theme-roller-view.js +0 -403
  27. package/plugins/theme-roller/config.js +0 -1
  28. package/plugins/theme-roller/plugin.js +0 -233
  29. package/plugins/theme-roller/plugin.json +0 -31
  30. package/plugins/theme-roller/public/active-theme.css +0 -0
  31. package/plugins/theme-roller/public/inject-head-late.html +0 -1
@@ -1,47 +0,0 @@
1
- import { getRoleContext } from '../lib/role-context.js';
2
- import { jbApi } from '../lib/api.js';
3
-
4
- export default {
5
- templateUrl: '/plugins/job-board/admin/templates/profile.html',
6
- async onMount($container) {
7
- const ctx = getRoleContext();
8
-
9
- // Only candidates have a profile
10
- if (!ctx.isCandidate && !ctx.isAdmin) {
11
- $container.find('#profile-body').html('<p class="text-muted">Profile management is for candidates only.</p>');
12
- return;
13
- }
14
-
15
- let profile = null;
16
- try {
17
- profile = await jbApi.profile.get();
18
- } catch {}
19
-
20
- const blueprint = [
21
- { name: 'headline', label: 'Headline', type: 'text', placeholder: 'e.g. Senior Frontend Developer' },
22
- { name: 'summary', label: 'Summary', type: 'textarea', placeholder: 'Brief professional summary...' },
23
- { name: 'skills', label: 'Skills', type: 'text', placeholder: 'e.g. JavaScript, React, Node.js' },
24
- { name: 'experience_years', label: 'Years of Experience', type: 'number' },
25
- { name: 'availability', label: 'Availability', type: 'select', options: ['immediately', '2 weeks', '1 month', '3 months', 'not available'] },
26
- { name: 'cv_url', label: 'CV URL', type: 'text', placeholder: 'https://...' }
27
- ];
28
-
29
- const form = F.create($container.find('#profile-form').get(0), { blueprint });
30
-
31
- if (profile?.data) {
32
- form.set(profile.data);
33
- }
34
-
35
- $container.find('#btn-save-profile').get(0)?.addEventListener('click', async () => {
36
- const data = form.get();
37
- try {
38
- await jbApi.profile.save(data);
39
- E.toast('Profile saved', { type: 'success' });
40
- } catch (err) {
41
- E.toast(err.message || 'Failed to save profile', { type: 'error' });
42
- }
43
- });
44
-
45
- I.scan($container.get(0));
46
- }
47
- };
@@ -1,6 +0,0 @@
1
- export default {
2
- emailNotifications: true,
3
- applicationStatuses: ['submitted', 'reviewed', 'shortlisted', 'interview', 'offered', 'rejected', 'withdrawn'],
4
- jobTypes: ['full-time', 'part-time', 'contract', 'freelance', 'internship'],
5
- industries: ['technology', 'finance', 'healthcare', 'education', 'retail', 'manufacturing', 'other']
6
- };
@@ -1,466 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- // Note: These paths assume the plugin is at plugins/{name}/plugin.js — two levels up is always
4
- // the repo root in this CMS layout. If the CMS directory structure changes, update these paths.
5
- import { createEntry, updateEntry, getEntry, listEntries, getCollection, createCollection } from '../../server/services/collections.js';
6
- import { createTransport, sendEmail, escapeHtml } from '../../server/services/email.js';
7
- import { getConfig } from '../../server/config.js';
8
-
9
- // ─── onEnable ────────────────────────────────────────────────────────────────
10
-
11
- export async function onEnable({ services }) {
12
- const { createCollection: svcCreateCollection, getCollection: svcGetCollection, createEntry: svcCreateEntry, listEntries: svcListEntries } = services.collections;
13
-
14
- const schemaDir = new URL('./schemas/', import.meta.url).pathname;
15
- const schemas = ['jb-companies', 'jb-jobs', 'jb-applications', 'jb-candidate-profiles', 'jb-agent-companies'];
16
-
17
- for (const slug of schemas) {
18
- const existing = await svcGetCollection(slug);
19
- if (existing) continue;
20
-
21
- const schemaPath = path.join(schemaDir, `${slug}.json`);
22
- const schema = JSON.parse(await fs.readFile(schemaPath, 'utf8'));
23
- await svcCreateCollection(schema);
24
- console.log(`[job-board] Created collection: ${slug}`);
25
- }
26
-
27
- const rolesToSeed = [
28
- { name: 'jb-company', label: 'Company', level: 5, permissions: ['jb-company'], badgeClass: 'badge-info' },
29
- { name: 'jb-agent', label: 'Agent', level: 5, permissions: ['jb-agent'], badgeClass: 'badge-warning' },
30
- { name: 'jb-candidate', label: 'Candidate', level: 6, permissions: ['jb-candidate'], badgeClass: 'badge-success' }
31
- ];
32
-
33
- const existingRoles = await svcListEntries('roles', { limit: 100 });
34
-
35
- for (const roleData of rolesToSeed) {
36
- const already = (existingRoles.entries || []).find(e => e.data?.name === roleData.name);
37
- if (already) continue;
38
-
39
- await svcCreateEntry('roles', roleData, { createdBy: null, source: 'plugin' });
40
- console.log(`[job-board] Created role: ${roleData.name}`);
41
- }
42
-
43
- if (services.roles && typeof services.roles.invalidate === 'function') {
44
- await services.roles.invalidate();
45
- }
46
- }
47
-
48
- // ─── onDisable ───────────────────────────────────────────────────────────────
49
-
50
- export async function onDisable() {
51
- console.log('[job-board] Plugin disabled. Data preserved.');
52
- }
53
-
54
- // ─── requireJobBoardRole ─────────────────────────────────────────────────────
55
-
56
- function requireJobBoardRole(...allowedRoles) {
57
- return async function (request, reply) {
58
- const user = request.user;
59
- if (!user) return reply.status(401).send({ error: 'Unauthorised' });
60
-
61
- if (user.role === 'admin' || user.role === 'manager') return;
62
-
63
- if (!allowedRoles.includes(user.role)) {
64
- return reply.status(403).send({ error: 'Forbidden' });
65
- }
66
- };
67
- }
68
-
69
- // ─── Fastify plugin ──────────────────────────────────────────────────────────
70
-
71
- export default async function jobBoardPlugin(fastify, options) {
72
- const { authenticate } = options.auth;
73
-
74
- // ── Jobs ──────────────────────────────────────────────────────────────────
75
-
76
- fastify.get('/jobs', { preHandler: [authenticate] }, async (request) => {
77
- const user = request.user;
78
- const all = await listEntries('jb-jobs', { limit: 200 });
79
- const entries = all.entries || [];
80
-
81
- if (user.role === 'admin' || user.role === 'manager') return entries;
82
-
83
- if (user.role === 'jb-candidate') {
84
- return entries.filter(e => e.data?.status === 'published');
85
- }
86
-
87
- if (user.role === 'jb-company') {
88
- const companies = await listEntries('jb-companies', { limit: 100 });
89
- const company = (companies.entries || []).find(e => e.data?.owner_id === user.id);
90
- if (!company) return [];
91
- return entries.filter(e => e.data?.company_id === company.id);
92
- }
93
-
94
- if (user.role === 'jb-agent') {
95
- const links = await listEntries('jb-agent-companies', { limit: 100 });
96
- const agentLinks = (links.entries || []).filter(e => e.data?.agent_id === user.id);
97
- const companyIds = new Set(agentLinks.map(l => l.data?.company_id));
98
- return entries.filter(e => companyIds.has(e.data?.company_id));
99
- }
100
-
101
- return [];
102
- });
103
-
104
- fastify.post('/jobs', { preHandler: [authenticate, requireJobBoardRole('jb-company', 'jb-agent')] }, async (request, reply) => {
105
- const user = request.user;
106
-
107
- let resolvedCompanyId;
108
-
109
- if (user.role === 'jb-company') {
110
- const companies = await listEntries('jb-companies', { limit: 100 });
111
- const company = (companies.entries || []).find(e => e.data?.owner_id === user.id);
112
- if (!company) return reply.status(403).send({ error: 'No company found for this user' });
113
- resolvedCompanyId = company.id;
114
- } else if (user.role === 'jb-agent') {
115
- const { company_id } = request.body;
116
- if (!company_id) return reply.status(400).send({ error: 'company_id is required' });
117
- const links = await listEntries('jb-agent-companies', { limit: 100 });
118
- const linked = (links.entries || []).some(e => e.data?.agent_id === user.id && e.data?.company_id === company_id);
119
- if (!linked) return reply.status(403).send({ error: 'Agent is not linked to that company' });
120
- resolvedCompanyId = company_id;
121
- } else {
122
- // admin/manager — trust body
123
- resolvedCompanyId = request.body.company_id;
124
- }
125
-
126
- const data = { ...request.body, company_id: resolvedCompanyId, posted_by: user.id, status: request.body.status || 'draft' };
127
- const entry = await createEntry('jb-jobs', data, { createdBy: user.id, source: 'plugin' });
128
- return reply.status(201).send(entry);
129
- });
130
-
131
- fastify.get('/jobs/:id', { preHandler: [authenticate] }, async (request, reply) => {
132
- const entry = await getEntry('jb-jobs', request.params.id);
133
- if (!entry) return reply.status(404).send({ error: 'Not found' });
134
- return entry;
135
- });
136
-
137
- fastify.put('/jobs/:id', { preHandler: [authenticate, requireJobBoardRole('jb-company', 'jb-agent')] }, async (request, reply) => {
138
- try {
139
- const user = request.user;
140
- const existing = await getEntry('jb-jobs', request.params.id);
141
- if (!existing) return reply.status(404).send({ error: 'Entry not found' });
142
-
143
- if (user.role !== 'admin' && user.role !== 'manager') {
144
- if (user.role === 'jb-company') {
145
- const companies = await listEntries('jb-companies', { limit: 100 });
146
- const company = (companies.entries || []).find(e => e.data?.owner_id === user.id);
147
- if (!company || existing.data?.company_id !== company.id) {
148
- return reply.status(403).send({ error: 'Forbidden' });
149
- }
150
- } else if (user.role === 'jb-agent') {
151
- const links = await listEntries('jb-agent-companies', { limit: 100 });
152
- const companyIds = new Set(
153
- (links.entries || [])
154
- .filter(e => e.data?.agent_id === user.id)
155
- .map(e => e.data?.company_id)
156
- );
157
- if (!companyIds.has(existing.data?.company_id)) {
158
- return reply.status(403).send({ error: 'Forbidden' });
159
- }
160
- } else {
161
- return reply.status(403).send({ error: 'Forbidden' });
162
- }
163
- }
164
-
165
- const entry = await updateEntry('jb-jobs', request.params.id, request.body);
166
- return entry;
167
- } catch (err) {
168
- const status = err.message === 'Entry not found' ? 404 : 400;
169
- return reply.status(status).send({ error: err.message });
170
- }
171
- });
172
-
173
- // ── Applications ──────────────────────────────────────────────────────────
174
-
175
- fastify.get('/applications', { preHandler: [authenticate] }, async (request) => {
176
- const user = request.user;
177
- const all = await listEntries('jb-applications', { limit: 200 });
178
- const entries = all.entries || [];
179
-
180
- if (user.role === 'admin' || user.role === 'manager') return entries;
181
-
182
- if (user.role === 'jb-candidate') {
183
- return entries.filter(e => e.data?.candidate_id === user.id);
184
- }
185
-
186
- if (user.role === 'jb-company') {
187
- const companies = await listEntries('jb-companies', { limit: 100 });
188
- const company = (companies.entries || []).find(e => e.data?.owner_id === user.id);
189
- if (!company) return [];
190
- return entries.filter(e => e.data?.company_id === company.id);
191
- }
192
-
193
- if (user.role === 'jb-agent') {
194
- const links = await listEntries('jb-agent-companies', { limit: 100 });
195
- const agentLinks = (links.entries || []).filter(e => e.data?.agent_id === user.id);
196
- const companyIds = new Set(agentLinks.map(l => l.data?.company_id));
197
- return entries.filter(e => companyIds.has(e.data?.company_id));
198
- }
199
-
200
- return [];
201
- });
202
-
203
- fastify.post('/applications', { preHandler: [authenticate, requireJobBoardRole('jb-candidate')] }, async (request, reply) => {
204
- const user = request.user;
205
- const { job_id, cover_letter, cv_url } = request.body;
206
-
207
- const job = await getEntry('jb-jobs', job_id);
208
- if (!job) return reply.status(404).send({ error: 'Job not found' });
209
-
210
- const data = {
211
- job_id,
212
- candidate_id: user.id,
213
- company_id: job.data?.company_id,
214
- cover_letter: cover_letter || '',
215
- cv_url: cv_url || '',
216
- status: 'submitted',
217
- applied_at: new Date().toISOString()
218
- };
219
- const entry = await createEntry('jb-applications', data, { createdBy: user.id, source: 'plugin' });
220
- return reply.status(201).send(entry);
221
- });
222
-
223
- fastify.get('/applications/:id', { preHandler: [authenticate] }, async (request, reply) => {
224
- const user = request.user;
225
- const entry = await getEntry('jb-applications', request.params.id);
226
- if (!entry) return reply.status(404).send({ error: 'Not found' });
227
-
228
- if (user.role === 'admin' || user.role === 'manager') return entry;
229
-
230
- if (user.role === 'jb-candidate') {
231
- if (entry.data?.candidate_id !== user.id) return reply.status(403).send({ error: 'Forbidden' });
232
- return entry;
233
- }
234
-
235
- if (user.role === 'jb-company') {
236
- const companies = await listEntries('jb-companies', { limit: 100 });
237
- const company = (companies.entries || []).find(e => e.data?.owner_id === user.id);
238
- if (!company || entry.data?.company_id !== company.id) return reply.status(403).send({ error: 'Forbidden' });
239
- return entry;
240
- }
241
-
242
- if (user.role === 'jb-agent') {
243
- const links = await listEntries('jb-agent-companies', { limit: 100 });
244
- const companyIds = new Set(
245
- (links.entries || [])
246
- .filter(e => e.data?.agent_id === user.id)
247
- .map(e => e.data?.company_id)
248
- );
249
- if (!companyIds.has(entry.data?.company_id)) return reply.status(403).send({ error: 'Forbidden' });
250
- return entry;
251
- }
252
-
253
- return reply.status(403).send({ error: 'Forbidden' });
254
- });
255
-
256
- fastify.put('/applications/:id/status', { preHandler: [authenticate, requireJobBoardRole('jb-company', 'jb-agent')] }, async (request, reply) => {
257
- const { status } = request.body;
258
- if (!status) return reply.status(400).send({ error: 'status is required' });
259
-
260
- try {
261
- const existing = await getEntry('jb-applications', request.params.id);
262
- if (!existing) return reply.status(404).send({ error: 'Not found' });
263
- const entry = await updateEntry('jb-applications', request.params.id, { ...existing.data, status });
264
- return entry;
265
- } catch (err) {
266
- return reply.status(400).send({ error: err.message });
267
- }
268
- });
269
-
270
- // ── Profile (candidates) ──────────────────────────────────────────────────
271
-
272
- fastify.get('/profile', { preHandler: [authenticate, requireJobBoardRole('jb-candidate')] }, async (request) => {
273
- const user = request.user;
274
- const all = await listEntries('jb-candidate-profiles', { limit: 100 });
275
- const profile = (all.entries || []).find(e => e.data?.user_id === user.id);
276
- return profile || null;
277
- });
278
-
279
- fastify.put('/profile', { preHandler: [authenticate, requireJobBoardRole('jb-candidate')] }, async (request, reply) => {
280
- const user = request.user;
281
- const all = await listEntries('jb-candidate-profiles', { limit: 100 });
282
- const existing = (all.entries || []).find(e => e.data?.user_id === user.id);
283
-
284
- const data = { ...request.body, user_id: user.id };
285
- if (existing) {
286
- return await updateEntry('jb-candidate-profiles', existing.id, data);
287
- } else {
288
- const entry = await createEntry('jb-candidate-profiles', data, { createdBy: user.id, source: 'plugin' });
289
- return reply.status(201).send(entry);
290
- }
291
- });
292
-
293
- // ── Companies ─────────────────────────────────────────────────────────────
294
-
295
- fastify.get('/companies', { preHandler: [authenticate] }, async (request) => {
296
- const user = request.user;
297
- const all = await listEntries('jb-companies', { limit: 200 });
298
- const entries = all.entries || [];
299
-
300
- if (user.role === 'admin' || user.role === 'manager') return entries;
301
-
302
- if (user.role === 'jb-company') {
303
- return entries.filter(e => e.data?.owner_id === user.id);
304
- }
305
-
306
- if (user.role === 'jb-agent') {
307
- const links = await listEntries('jb-agent-companies', { limit: 100 });
308
- const agentLinks = (links.entries || []).filter(e => e.data?.agent_id === user.id);
309
- const companyIds = new Set(agentLinks.map(l => l.data?.company_id));
310
- return entries.filter(e => companyIds.has(e.id));
311
- }
312
-
313
- return [];
314
- });
315
-
316
- fastify.post('/companies', { preHandler: [authenticate, requireJobBoardRole('jb-company')] }, async (request, reply) => {
317
- const user = request.user;
318
- const data = { ...request.body, owner_id: user.id, status: 'active' };
319
- const entry = await createEntry('jb-companies', data, { createdBy: user.id, source: 'plugin' });
320
- return reply.status(201).send(entry);
321
- });
322
-
323
- fastify.get('/companies/:id', { preHandler: [authenticate] }, async (request, reply) => {
324
- const entry = await getEntry('jb-companies', request.params.id);
325
- if (!entry) return reply.status(404).send({ error: 'Not found' });
326
- return entry;
327
- });
328
-
329
- fastify.put('/companies/:id', { preHandler: [authenticate, requireJobBoardRole('jb-company')] }, async (request, reply) => {
330
- try {
331
- const user = request.user;
332
- const existing = await getEntry('jb-companies', request.params.id);
333
- if (!existing) return reply.status(404).send({ error: 'Entry not found' });
334
-
335
- if (user.role !== 'admin' && user.role !== 'manager') {
336
- if (existing.data?.owner_id !== user.id) {
337
- return reply.status(403).send({ error: 'Forbidden' });
338
- }
339
- }
340
-
341
- return await updateEntry('jb-companies', request.params.id, request.body);
342
- } catch (err) {
343
- const status = err.message === 'Entry not found' ? 404 : 400;
344
- return reply.status(status).send({ error: err.message });
345
- }
346
- });
347
-
348
- // ── Agent links ───────────────────────────────────────────────────────────
349
-
350
- fastify.post('/agent-links', { preHandler: [authenticate, requireJobBoardRole('jb-agent')] }, async (request, reply) => {
351
- const user = request.user;
352
- const { company_id, role: roleLabel } = request.body;
353
- if (!company_id) return reply.status(400).send({ error: 'company_id is required' });
354
-
355
- const data = {
356
- agent_id: user.id,
357
- company_id,
358
- role: roleLabel || 'agent',
359
- added_at: new Date().toISOString()
360
- };
361
- const entry = await createEntry('jb-agent-companies', data, { createdBy: user.id, source: 'plugin' });
362
- return reply.status(201).send(entry);
363
- });
364
-
365
- // -------------------------------------------------------------------------
366
- // Workflow automation — hook listeners
367
- // -------------------------------------------------------------------------
368
- const { on: hookOn } = options.hooks;
369
-
370
- // Helper: get email transport (lazy-initialised per plugin load)
371
- let _transport = null;
372
- async function getTransport() {
373
- if (_transport) return _transport;
374
- const smtp = getConfig('site').smtp || {};
375
- _transport = await createTransport(smtp);
376
- return _transport;
377
- }
378
-
379
- // 1. New application → email company
380
- hookOn('collection:entryCreated', async ({ slug, data }) => {
381
- if (slug !== 'jb-applications') return;
382
-
383
- try {
384
- // Get job to find company
385
- const job = await getEntry('jb-jobs', data.job_id);
386
- if (!job) return;
387
-
388
- // Get company to find owner email
389
- const companies = await listEntries('jb-companies', { limit: 100 });
390
- const company = (companies.entries || []).find(e => e.id === data.company_id);
391
- if (!company) return;
392
-
393
- // Send to the site admin email as a fallback
394
- const site = getConfig('site') || {};
395
- const toEmail = site.smtp?.fromAddress || site.contactEmail;
396
- if (!toEmail) return;
397
-
398
- const transport = await getTransport();
399
- const smtp = site.smtp || {};
400
- await sendEmail(transport, {
401
- from: smtp.fromAddress || 'noreply@example.com',
402
- fromName: smtp.fromName || site.title || 'Job Board',
403
- to: toEmail,
404
- subject: `New application for: ${job.data?.title || 'a job'}`,
405
- html: `<p>A new application has been submitted for <strong>${escapeHtml(job.data?.title || '')}</strong>.</p><p>Status: submitted</p>`,
406
- text: `New application for: ${job.data?.title || 'a job'}. Status: submitted`
407
- });
408
- } catch (err) {
409
- fastify.log.error(`[job-board] Failed to send new application email: ${err.message}`);
410
- }
411
- });
412
-
413
- // 2. Application status change → email candidate
414
- hookOn('collection:entryUpdated', async ({ slug, entryId, data }) => {
415
- if (slug !== 'jb-applications') return;
416
- if (!data.status || !data.candidate_id) return;
417
-
418
- try {
419
- const site = getConfig('site') || {};
420
- const smtp = site.smtp || {};
421
- const fromAddress = smtp.fromAddress || 'noreply@example.com';
422
-
423
- // Look up candidate's email from users service
424
- const { getUserById } = await import('../../server/services/users.js');
425
- const candidate = await getUserById(data.candidate_id).catch(() => null);
426
- const toEmail = candidate?.email;
427
- if (!toEmail) return; // No email address on record — skip
428
-
429
- const transport = await getTransport();
430
- await sendEmail(transport, {
431
- from: fromAddress,
432
- fromName: smtp.fromName || site.title || 'Job Board',
433
- to: toEmail,
434
- subject: `Your application status: ${data.status}`,
435
- html: `<p>Your job application status has been updated to: <strong>${escapeHtml(data.status)}</strong>.</p>`,
436
- text: `Your application status: ${data.status}`
437
- });
438
- } catch (err) {
439
- fastify.log.error(`[job-board] Failed to send status update email: ${err.message}`);
440
- }
441
- });
442
-
443
- // 3. Job closed → auto-reject pending applications
444
- hookOn('collection:entryUpdated', async ({ slug, entryId, data }) => {
445
- if (slug !== 'jb-jobs') return;
446
- if (data.status !== 'closed') return;
447
-
448
- try {
449
- const all = await listEntries('jb-applications', { limit: 500 });
450
- const pending = (all.entries || []).filter(a =>
451
- a.data?.job_id === entryId &&
452
- ['submitted', 'reviewed'].includes(a.data?.status)
453
- );
454
-
455
- for (const app of pending) {
456
- await updateEntry('jb-applications', app.id, { ...app.data, status: 'rejected' });
457
- }
458
-
459
- if (pending.length) {
460
- fastify.log.info(`[job-board] Auto-rejected ${pending.length} applications for closed job ${entryId}`);
461
- }
462
- } catch (err) {
463
- fastify.log.error(`[job-board] Failed to auto-reject applications: ${err.message}`);
464
- }
465
- });
466
- }
@@ -1,40 +0,0 @@
1
- {
2
- "name": "job-board",
3
- "displayName": "Job Board",
4
- "version": "1.0.0",
5
- "description": "Full-featured job board with company, agent, and candidate roles.",
6
- "author": "Domma CMS",
7
- "date": "2026-03-23",
8
- "icon": "briefcase",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "job-board",
13
- "text": "Job Board",
14
- "icon": "briefcase",
15
- "url": "#/job-board",
16
- "section": "#/job-board",
17
- "permissions": ["jb-company", "jb-agent", "jb-candidate"]
18
- }
19
- ],
20
- "routes": [
21
- { "path": "/job-board", "view": "jbDashboard", "title": "Job Board - Dashboard" },
22
- { "path": "/job-board/jobs", "view": "jbJobs", "title": "Job Board - Jobs" },
23
- { "path": "/job-board/jobs/new", "view": "jbJobEditor", "title": "Job Board - New Job" },
24
- { "path": "/job-board/jobs/edit/:id", "view": "jbJobEditor", "title": "Job Board - Edit Job" },
25
- { "path": "/job-board/applications", "view": "jbApplications", "title": "Job Board - Applications" },
26
- { "path": "/job-board/applications/:id", "view": "jbApplicationDetail", "title": "Job Board - Application" },
27
- { "path": "/job-board/companies", "view": "jbCompanies", "title": "Job Board - Companies" },
28
- { "path": "/job-board/profile", "view": "jbProfile", "title": "Job Board - My Profile" }
29
- ],
30
- "views": {
31
- "jbDashboard": { "entry": "job-board/admin/views/dashboard.js", "exportName": "default" },
32
- "jbJobs": { "entry": "job-board/admin/views/jobs.js", "exportName": "default" },
33
- "jbJobEditor": { "entry": "job-board/admin/views/job-editor.js", "exportName": "default" },
34
- "jbApplications": { "entry": "job-board/admin/views/applications.js", "exportName": "default" },
35
- "jbApplicationDetail":{ "entry": "job-board/admin/views/application-detail.js", "exportName": "default" },
36
- "jbCompanies": { "entry": "job-board/admin/views/companies.js", "exportName": "default" },
37
- "jbProfile": { "entry": "job-board/admin/views/profile.js", "exportName": "default" }
38
- }
39
- }
40
- }
@@ -1,17 +0,0 @@
1
- {
2
- "slug": "jb-agent-companies",
3
- "title": "JB Agent Companies",
4
- "description": "Links between agents and companies",
5
- "fields": [
6
- { "name": "agent_id", "label": "Agent User ID", "type": "text", "required": true },
7
- { "name": "company_id", "label": "Company ID", "type": "text", "required": true },
8
- { "name": "role", "label": "Role", "type": "text" },
9
- { "name": "added_at", "label": "Added At", "type": "text" }
10
- ],
11
- "api": {
12
- "create": { "enabled": false, "access": "admin" },
13
- "read": { "enabled": false, "access": "admin" },
14
- "update": { "enabled": false, "access": "admin" },
15
- "delete": { "enabled": false, "access": "admin" }
16
- }
17
- }
@@ -1,20 +0,0 @@
1
- {
2
- "slug": "jb-applications",
3
- "title": "JB Applications",
4
- "description": "Job applications",
5
- "fields": [
6
- { "name": "job_id", "label": "Job ID", "type": "text", "required": true },
7
- { "name": "candidate_id", "label": "Candidate ID", "type": "text", "required": true },
8
- { "name": "company_id", "label": "Company ID", "type": "text", "required": true },
9
- { "name": "cover_letter", "label": "Cover Letter", "type": "textarea" },
10
- { "name": "cv_url", "label": "CV URL", "type": "text" },
11
- { "name": "status", "label": "Status", "type": "text" },
12
- { "name": "applied_at", "label": "Applied At", "type": "text" }
13
- ],
14
- "api": {
15
- "create": { "enabled": false, "access": "admin" },
16
- "read": { "enabled": false, "access": "admin" },
17
- "update": { "enabled": false, "access": "admin" },
18
- "delete": { "enabled": false, "access": "admin" }
19
- }
20
- }
@@ -1,20 +0,0 @@
1
- {
2
- "slug": "jb-candidate-profiles",
3
- "title": "JB Candidate Profiles",
4
- "description": "Candidate profiles for job board",
5
- "fields": [
6
- { "name": "user_id", "label": "User ID", "type": "text", "required": true },
7
- { "name": "headline", "label": "Headline", "type": "text" },
8
- { "name": "summary", "label": "Summary", "type": "textarea" },
9
- { "name": "cv_url", "label": "CV URL", "type": "text" },
10
- { "name": "skills", "label": "Skills", "type": "text" },
11
- { "name": "experience_years", "label": "Years Experience", "type": "number" },
12
- { "name": "availability", "label": "Availability", "type": "text" }
13
- ],
14
- "api": {
15
- "create": { "enabled": false, "access": "admin" },
16
- "read": { "enabled": false, "access": "admin" },
17
- "update": { "enabled": false, "access": "admin" },
18
- "delete": { "enabled": false, "access": "admin" }
19
- }
20
- }
@@ -1,21 +0,0 @@
1
- {
2
- "slug": "jb-companies",
3
- "title": "JB Companies",
4
- "description": "Job board companies",
5
- "fields": [
6
- { "name": "name", "label": "Company Name", "type": "text", "required": true },
7
- { "name": "slug", "label": "Slug", "type": "text", "required": true },
8
- { "name": "logo", "label": "Logo URL", "type": "text" },
9
- { "name": "industry", "label": "Industry", "type": "text" },
10
- { "name": "size", "label": "Size", "type": "text" },
11
- { "name": "location", "label": "Location", "type": "text" },
12
- { "name": "owner_id", "label": "Owner User ID", "type": "text" },
13
- { "name": "status", "label": "Status", "type": "text" }
14
- ],
15
- "api": {
16
- "create": { "enabled": false, "access": "admin" },
17
- "read": { "enabled": false, "access": "admin" },
18
- "update": { "enabled": false, "access": "admin" },
19
- "delete": { "enabled": false, "access": "admin" }
20
- }
21
- }