domma-cms 0.6.15 → 0.6.16

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 (39) hide show
  1. package/admin/js/app.js +4 -4
  2. package/admin/js/config/sidebar-config.js +1 -1
  3. package/admin/js/lib/markdown-toolbar.js +8 -6
  4. package/config/plugins.json +4 -0
  5. package/package.json +1 -1
  6. package/plugins/analytics/stats.json +1 -1
  7. package/plugins/job-board/admin/templates/application-detail.html +40 -0
  8. package/plugins/job-board/admin/templates/applications.html +10 -0
  9. package/plugins/job-board/admin/templates/companies.html +24 -0
  10. package/plugins/job-board/admin/templates/dashboard.html +36 -0
  11. package/plugins/job-board/admin/templates/job-editor.html +17 -0
  12. package/plugins/job-board/admin/templates/jobs.html +15 -0
  13. package/plugins/job-board/admin/templates/profile.html +17 -0
  14. package/plugins/job-board/admin/views/application-detail.js +62 -0
  15. package/plugins/job-board/admin/views/applications.js +47 -0
  16. package/plugins/job-board/admin/views/companies.js +104 -0
  17. package/plugins/job-board/admin/views/dashboard.js +88 -0
  18. package/plugins/job-board/admin/views/job-editor.js +86 -0
  19. package/plugins/job-board/admin/views/jobs.js +53 -0
  20. package/plugins/job-board/admin/views/profile.js +47 -0
  21. package/plugins/job-board/config.js +6 -0
  22. package/plugins/job-board/plugin.js +466 -0
  23. package/plugins/job-board/plugin.json +40 -0
  24. package/plugins/job-board/schemas/jb-agent-companies.json +17 -0
  25. package/plugins/job-board/schemas/jb-applications.json +20 -0
  26. package/plugins/job-board/schemas/jb-candidate-profiles.json +20 -0
  27. package/plugins/job-board/schemas/jb-companies.json +21 -0
  28. package/plugins/job-board/schemas/jb-jobs.json +23 -0
  29. package/server/routes/api/collections.js +4 -0
  30. package/server/routes/api/plugins.js +9 -1
  31. package/server/services/plugins.js +30 -0
  32. package/plugins/example-analytics/admin/templates/analytics.html +0 -10
  33. package/plugins/example-analytics/admin/views/analytics.js +0 -51
  34. package/plugins/example-analytics/config.js +0 -6
  35. package/plugins/example-analytics/plugin.js +0 -58
  36. package/plugins/example-analytics/plugin.json +0 -45
  37. package/plugins/example-analytics/public/inject-body.html +0 -14
  38. package/plugins/example-analytics/public/inject-head.html +0 -1
  39. package/plugins/example-analytics/stats.json +0 -24
@@ -0,0 +1,104 @@
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/companies.html',
6
+ async onMount($container) {
7
+ const ctx = getRoleContext();
8
+
9
+ // Candidates don't see companies
10
+ if (ctx.isCandidate) {
11
+ $container.find('#companies-body').html('<p class="text-muted">Companies are not available in your view.</p>');
12
+ return;
13
+ }
14
+
15
+ // Hide create button for agents (they link, not create)
16
+ if (ctx.isAgent) {
17
+ $container.find('#btn-create-company').hide();
18
+ $container.find('#agent-link-section').show();
19
+ }
20
+
21
+ const loadCompanies = async () => {
22
+ let companies = [];
23
+ try {
24
+ const res = await jbApi.companies.list();
25
+ companies = Array.isArray(res) ? res : (res?.entries || []);
26
+ } catch {
27
+ E.toast('Failed to load companies', { type: 'error' });
28
+ return;
29
+ }
30
+
31
+ T.create($container.find('#companies-table').get(0), {
32
+ data: companies,
33
+ columns: [
34
+ { key: 'data.name', label: 'Company Name' },
35
+ { key: 'data.industry', label: 'Industry' },
36
+ { key: 'data.location', label: 'Location' },
37
+ { key: 'data.status', label: 'Status' }
38
+ ]
39
+ });
40
+ };
41
+
42
+ await loadCompanies();
43
+
44
+ // Create company (jb-company role only)
45
+ if (ctx.isCompany || ctx.isAdmin) {
46
+ const modal = E.modal({ title: 'Create Company', size: 'md' });
47
+
48
+ const formContainer = document.createElement('div');
49
+ const form = F.create(formContainer, {
50
+ blueprint: [
51
+ { name: 'name', label: 'Company Name', type: 'text', required: true },
52
+ { name: 'industry', label: 'Industry', type: 'text' },
53
+ { name: 'location', label: 'Location', type: 'text' },
54
+ { name: 'size', label: 'Size', type: 'text', placeholder: 'e.g. 1-10, 50-100' }
55
+ ]
56
+ });
57
+
58
+ const saveBtn = document.createElement('button');
59
+ saveBtn.className = 'btn btn-primary mt-3';
60
+ saveBtn.textContent = 'Create Company';
61
+ formContainer.appendChild(saveBtn);
62
+
63
+ modal.element.appendChild(formContainer);
64
+
65
+ saveBtn.addEventListener('click', async () => {
66
+ const data = form.get();
67
+ if (!data.name) {
68
+ E.toast('Company name is required', { type: 'error' });
69
+ return;
70
+ }
71
+ try {
72
+ await jbApi.companies.create(data);
73
+ E.toast('Company created', { type: 'success' });
74
+ modal.close();
75
+ await loadCompanies();
76
+ } catch (err) {
77
+ E.toast(err.message || 'Failed to create company', { type: 'error' });
78
+ }
79
+ });
80
+
81
+ $container.find('#btn-create-company').get(0)?.addEventListener('click', () => modal.open());
82
+ }
83
+
84
+ // Agent link to company
85
+ if (ctx.isAgent) {
86
+ $container.find('#btn-link-company').get(0)?.addEventListener('click', async () => {
87
+ const companyId = $container.find('#link-company-id').val()?.trim();
88
+ if (!companyId) {
89
+ E.toast('Company ID is required', { type: 'error' });
90
+ return;
91
+ }
92
+ try {
93
+ await jbApi.agentLinks.link({ company_id: companyId, role: 'agent' });
94
+ E.toast('Linked to company', { type: 'success' });
95
+ await loadCompanies();
96
+ } catch (err) {
97
+ E.toast(err.message || 'Failed to link company', { type: 'error' });
98
+ }
99
+ });
100
+ }
101
+
102
+ I.scan($container.get(0));
103
+ }
104
+ };
@@ -0,0 +1,88 @@
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/dashboard.html',
6
+ async onMount($container) {
7
+ const ctx = getRoleContext();
8
+
9
+ // Load data
10
+ let jobs = [], applications = [], companies = [];
11
+ try {
12
+ [jobs, applications] = await Promise.all([
13
+ jbApi.jobs.list().catch(() => []),
14
+ jbApi.applications.list().catch(() => [])
15
+ ]);
16
+ if (ctx.isAgent || ctx.isAdmin) {
17
+ companies = await jbApi.companies.list().catch(() => []);
18
+ }
19
+ } catch {}
20
+
21
+ // Ensure arrays
22
+ jobs = Array.isArray(jobs) ? jobs : (jobs?.entries || []);
23
+ applications = Array.isArray(applications) ? applications : (applications?.entries || []);
24
+ companies = Array.isArray(companies) ? companies : (companies?.entries || []);
25
+
26
+ // Stats
27
+ const activeJobs = jobs.filter(j => j.data?.status === 'published').length;
28
+ const totalJobs = jobs.length;
29
+ const totalApps = applications.length;
30
+ const newApps = applications.filter(a => a.data?.status === 'submitted').length;
31
+
32
+ // Render stats
33
+ $container.find('#stat-jobs').text(ctx.isCandidate ? totalApps : totalJobs);
34
+ $container.find('#stat-apps').text(ctx.isCandidate ? newApps : totalApps);
35
+ $container.find('#stat-companies').text(companies.length);
36
+
37
+ // Show/hide company stat based on role
38
+ if (!ctx.isAgent && !ctx.isAdmin) {
39
+ $container.find('#stat-companies-card').hide();
40
+ }
41
+
42
+ // Role-specific heading
43
+ const headings = {
44
+ 'jb-company': 'Company Dashboard',
45
+ 'jb-agent': 'Agent Dashboard',
46
+ 'jb-candidate':'My Job Applications',
47
+ 'admin': 'Job Board Overview',
48
+ 'manager': 'Job Board Overview'
49
+ };
50
+ $container.find('#jb-heading').text(headings[ctx.role] || 'Job Board');
51
+
52
+ // Role-specific quick links
53
+ const $links = $container.find('#quick-links');
54
+ if (ctx.isCompany || ctx.isAgent) {
55
+ $links.html(`
56
+ <a href="#/job-board/jobs/new" class="btn btn-primary">Post a Job</a>
57
+ <a href="#/job-board/applications" class="btn btn-secondary ml-2">View Applications</a>
58
+ `);
59
+ } else if (ctx.isCandidate) {
60
+ $links.html(`
61
+ <a href="#/job-board/jobs" class="btn btn-primary">Browse Jobs</a>
62
+ <a href="#/job-board/profile" class="btn btn-secondary ml-2">My Profile</a>
63
+ `);
64
+ } else {
65
+ $links.html(`
66
+ <a href="#/job-board/jobs" class="btn btn-primary">All Jobs</a>
67
+ <a href="#/job-board/companies" class="btn btn-secondary ml-2">Companies</a>
68
+ `);
69
+ }
70
+
71
+ // Recent applications table
72
+ const recentApps = applications.slice(0, 5);
73
+ if (recentApps.length) {
74
+ T.create($container.find('#recent-apps-table').get(0), {
75
+ data: recentApps,
76
+ columns: [
77
+ { key: 'data.job_id', label: 'Job ID' },
78
+ { key: 'data.status', label: 'Status' },
79
+ { key: 'data.applied_at', label: 'Applied', render: v => v ? new Date(v).toLocaleDateString() : '—' }
80
+ ]
81
+ });
82
+ } else {
83
+ $container.find('#recent-apps-section').hide();
84
+ }
85
+
86
+ I.scan($container.get(0));
87
+ }
88
+ };
@@ -0,0 +1,86 @@
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/job-editor.html',
6
+ async onMount($container) {
7
+ const ctx = getRoleContext();
8
+
9
+ // Get job ID from hash if editing
10
+ const hash = window.location.hash;
11
+ const editMatch = hash.match(/\/job-board\/jobs\/edit\/([^/]+)/);
12
+ const jobId = editMatch ? editMatch[1] : null;
13
+
14
+ let job = null;
15
+ if (jobId) {
16
+ try {
17
+ job = await jbApi.jobs.get(jobId);
18
+ $container.find('#view-title').text('Edit Job');
19
+ } catch {
20
+ E.toast('Failed to load job', { type: 'error' });
21
+ return;
22
+ }
23
+ }
24
+
25
+ // Load companies for agent/admin selector
26
+ let companies = [];
27
+ if (ctx.isAgent || ctx.isAdmin) {
28
+ try {
29
+ const res = await jbApi.companies.list();
30
+ companies = Array.isArray(res) ? res : (res?.entries || []);
31
+ } catch {}
32
+ }
33
+
34
+ // Build form blueprint
35
+ const blueprint = [
36
+ { name: 'title', label: 'Job Title', type: 'text', required: true },
37
+ { name: 'description', label: 'Description', type: 'textarea' },
38
+ { name: 'type', label: 'Job Type', type: 'select', options: ['full-time', 'part-time', 'contract', 'freelance', 'internship'] },
39
+ { name: 'salary_min', label: 'Min Salary', type: 'number' },
40
+ { name: 'salary_max', label: 'Max Salary', type: 'number' },
41
+ { name: 'remote', label: 'Remote', type: 'checkbox' },
42
+ { name: 'status', label: 'Status', type: 'select', options: ['draft', 'published', 'closed'] },
43
+ { name: 'closes_at', label: 'Closes At', type: 'text', placeholder: 'YYYY-MM-DD' }
44
+ ];
45
+
46
+ // Inject company selector for agents/admins
47
+ if ((ctx.isAgent || ctx.isAdmin) && companies.length) {
48
+ blueprint.splice(1, 0, {
49
+ name: 'company_id',
50
+ label: 'Company',
51
+ type: 'select',
52
+ options: companies.map(c => ({ value: c.id, label: c.data?.name || c.id }))
53
+ });
54
+ }
55
+
56
+ const form = F.create($container.find('#job-form').get(0), { blueprint });
57
+
58
+ // Populate form if editing
59
+ if (job?.data) {
60
+ form.set(job.data);
61
+ }
62
+
63
+ // Save handler
64
+ $container.find('#btn-save').get(0)?.addEventListener('click', async () => {
65
+ const data = form.get();
66
+ if (!data.title) {
67
+ E.toast('Job title is required', { type: 'error' });
68
+ return;
69
+ }
70
+
71
+ try {
72
+ if (jobId) {
73
+ await jbApi.jobs.update(jobId, data);
74
+ } else {
75
+ await jbApi.jobs.create(data);
76
+ }
77
+ E.toast('Job saved', { type: 'success' });
78
+ R.navigate('/job-board/jobs');
79
+ } catch (err) {
80
+ E.toast(err.message || 'Failed to save job', { type: 'error' });
81
+ }
82
+ });
83
+
84
+ I.scan($container.get(0));
85
+ }
86
+ };
@@ -0,0 +1,53 @@
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/jobs.html',
6
+ async onMount($container) {
7
+ const ctx = getRoleContext();
8
+
9
+ // Candidates cannot post jobs
10
+ if (ctx.isCandidate) {
11
+ $container.find('#btn-post-job').hide();
12
+ }
13
+
14
+ let jobs = [];
15
+ try {
16
+ const res = await jbApi.jobs.list();
17
+ jobs = Array.isArray(res) ? res : (res?.entries || []);
18
+ } catch {
19
+ E.toast('Failed to load jobs', { type: 'error' });
20
+ return;
21
+ }
22
+
23
+ const statusBadge = (status) => {
24
+ const map = {
25
+ published: 'badge-success',
26
+ draft: 'badge-secondary',
27
+ closed: 'badge-danger'
28
+ };
29
+ return `<span class="badge ${map[status] || 'badge-secondary'}">${status || '—'}</span>`;
30
+ };
31
+
32
+ T.create($container.find('#jobs-table').get(0), {
33
+ data: jobs,
34
+ columns: [
35
+ { key: 'data.title', label: 'Title' },
36
+ { key: 'data.status', label: 'Status', render: v => statusBadge(v) },
37
+ { key: 'data.type', label: 'Type' },
38
+ { key: 'data.remote', label: 'Remote', render: v => v ? 'Yes' : 'No' },
39
+ {
40
+ label: 'Actions',
41
+ render: (_, row) => {
42
+ if (ctx.isCandidate) {
43
+ return `<a href="#/job-board/jobs/${row.id}" class="btn btn-sm btn-secondary">View</a>`;
44
+ }
45
+ return `<a href="#/job-board/jobs/edit/${row.id}" class="btn btn-sm btn-primary">Edit</a>`;
46
+ }
47
+ }
48
+ ]
49
+ });
50
+
51
+ I.scan($container.get(0));
52
+ }
53
+ };
@@ -0,0 +1,47 @@
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
+ };
@@ -0,0 +1,6 @@
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
+ };