@objectql/create 3.0.1 → 4.0.1
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/dist/bin.js +7 -0
- package/package.json +1 -1
- package/templates/enterprise/CHANGELOG.md +10 -0
- package/templates/enterprise/jest.config.js +8 -0
- package/templates/enterprise/package.json +4 -2
- package/templates/enterprise/src/core/index.ts +8 -0
- package/templates/enterprise/src/extensions/user.ts +8 -0
- package/templates/enterprise/src/index.ts +10 -2
- package/templates/enterprise/src/modules/crm/index.ts +8 -0
- package/templates/enterprise/src/modules/finance/index.ts +8 -0
- package/templates/enterprise/src/modules/hr/index.ts +8 -0
- package/templates/enterprise/src/modules/project/index.ts +8 -0
- package/templates/enterprise/src/plugins/audit/audit.plugin.ts +40 -19
- package/templates/enterprise/src/plugins/audit/index.ts +9 -4
- package/templates/enterprise/src/shared/constants.ts +8 -0
- package/templates/enterprise/src/shared/utils.ts +8 -0
- package/templates/enterprise/src/shared/validators.ts +8 -0
- package/templates/enterprise/tsconfig.json +5 -0
- package/templates/enterprise/tsconfig.tsbuildinfo +1 -1
- package/templates/hello-world/CHANGELOG.md +8 -0
- package/templates/hello-world/package.json +1 -1
- package/templates/hello-world/src/index.ts +8 -0
- package/templates/starter/CHANGELOG.md +10 -0
- package/templates/starter/jest.config.js +8 -0
- package/templates/starter/package.json +3 -2
- package/templates/starter/src/modules/projects/projects.action.ts +203 -346
- package/templates/starter/src/modules/projects/projects.hook.ts +106 -263
- package/templates/starter/src/seed.ts +9 -1
- package/templates/starter/tsconfig.tsbuildinfo +1 -1
- package/templates/enterprise/__tests__/data-api.test.ts +0 -546
- package/templates/enterprise/__tests__/data-api.test.ts.backup +0 -526
- package/templates/enterprise/__tests__/metadata-api.test.ts +0 -307
- package/templates/enterprise/__tests__/metadata-loading.test.ts +0 -250
- package/templates/starter/__tests__/projects-hooks-actions.test.ts +0 -490
|
@@ -1,472 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { ActionDefinition } from '@objectql/types';
|
|
2
|
-
import { Projects } from '../../types';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
|
-
* Project Actions -
|
|
12
|
+
* Project Actions - Custom Business Operations
|
|
6
13
|
*
|
|
7
|
-
* This file
|
|
8
|
-
*
|
|
9
|
-
* 2. Global actions (operate on collections)
|
|
10
|
-
* 3. Input validation
|
|
11
|
-
* 4. Multi-step business logic
|
|
12
|
-
* 5. Error handling
|
|
14
|
+
* This file implements custom RPC actions for the Project object.
|
|
15
|
+
* Actions are explicitly invoked by users or systems (not triggered by CRUD).
|
|
13
16
|
*/
|
|
14
17
|
|
|
18
|
+
// ===== RECORD ACTIONS =====
|
|
19
|
+
// These actions operate on a specific project record (require ID)
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
22
|
+
* Complete Action
|
|
23
|
+
*
|
|
24
|
+
* Marks a project as completed.
|
|
25
|
+
* Type: Record Action (operates on a single project)
|
|
17
26
|
*/
|
|
18
27
|
interface CompleteInput {
|
|
19
28
|
comment?: string;
|
|
20
|
-
completion_date?: Date;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export const complete: ActionDefinition<Projects, CompleteInput> = {
|
|
33
|
-
handler: async ({ id, input, api, user }) => {
|
|
34
|
-
const { comment, completion_date } = input;
|
|
35
|
-
|
|
36
|
-
console.log(`[Action] Completing project ${id} by ${user?.id}`);
|
|
37
|
-
|
|
38
|
-
// 1. Validate - Fetch current state
|
|
39
|
-
const project = await api.findOne('projects', id!);
|
|
40
|
-
|
|
31
|
+
export const complete: ActionDefinition<any, CompleteInput> = {
|
|
32
|
+
handler: async ({ id, input, api, user, objectName }) => {
|
|
33
|
+
// Validate id is provided
|
|
34
|
+
if (!id) {
|
|
35
|
+
throw new Error('Project ID is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fetch current project state
|
|
39
|
+
const project = await api.findOne(objectName, id);
|
|
41
40
|
if (!project) {
|
|
42
41
|
throw new Error('Project not found');
|
|
43
42
|
}
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
// Validate project is not already completed
|
|
45
45
|
if (project.status === 'completed') {
|
|
46
46
|
throw new Error('Project is already completed');
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
if (!user?.isAdmin && project.owner !== user?.id) {
|
|
52
|
-
throw new Error('Only the project owner or admin can complete the project');
|
|
53
|
-
}
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
// 3. Perform update with atomic operation
|
|
57
|
-
const updateData: any = {
|
|
48
|
+
|
|
49
|
+
// Update project status to completed
|
|
50
|
+
await api.update(objectName, id, {
|
|
58
51
|
status: 'completed',
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Add completion comment to description
|
|
63
|
-
if (comment) {
|
|
64
|
-
updateData.description = project.description
|
|
65
|
-
? `${project.description}\n\n[Completed on ${new Date().toISOString()}]: ${comment}`
|
|
66
|
-
: `[Completed on ${new Date().toISOString()}]: ${comment}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
await api.update('projects', id!, updateData);
|
|
70
|
-
|
|
71
|
-
// 4. Optional: Create completion record or notification
|
|
72
|
-
/*
|
|
73
|
-
await api.create('project_completions', {
|
|
74
|
-
project_id: id,
|
|
75
|
-
completed_by: user?.id,
|
|
52
|
+
completed_by: user?.id || 'system',
|
|
76
53
|
completed_at: new Date(),
|
|
77
|
-
|
|
54
|
+
completion_comment: input.comment
|
|
78
55
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
success: true,
|
|
84
|
-
message: "Project completed successfully",
|
|
85
|
-
project_id: id,
|
|
86
|
-
completed_at: updateData.end_date
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
message: `Project "${project.name}" completed successfully`
|
|
87
60
|
};
|
|
88
61
|
}
|
|
89
62
|
};
|
|
90
63
|
|
|
91
64
|
/**
|
|
92
|
-
*
|
|
65
|
+
* Approve Action
|
|
66
|
+
*
|
|
67
|
+
* Approves a planned project and moves it to in_progress status.
|
|
68
|
+
* Type: Record Action (operates on a single project)
|
|
93
69
|
*/
|
|
94
70
|
interface ApproveInput {
|
|
95
71
|
comment: string;
|
|
96
72
|
}
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
handler: async ({ id, input, api, user }) => {
|
|
108
|
-
const { comment } = input;
|
|
109
|
-
|
|
110
|
-
// 1. Validate input
|
|
111
|
-
if (!comment || comment.trim().length === 0) {
|
|
74
|
+
export const approve: ActionDefinition<any, ApproveInput> = {
|
|
75
|
+
handler: async ({ id, input, api, user, objectName }) => {
|
|
76
|
+
// Validate id is provided
|
|
77
|
+
if (!id) {
|
|
78
|
+
throw new Error('Project ID is required');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate approval comment is required
|
|
82
|
+
if (!input.comment || input.comment.trim() === '') {
|
|
112
83
|
throw new Error('Approval comment is required');
|
|
113
84
|
}
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
const project = await api.findOne(
|
|
117
|
-
|
|
85
|
+
|
|
86
|
+
// Fetch current project state
|
|
87
|
+
const project = await api.findOne(objectName, id);
|
|
118
88
|
if (!project) {
|
|
119
89
|
throw new Error('Project not found');
|
|
120
90
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 3. Check budget threshold
|
|
127
|
-
if (project.budget > 100000 && !user?.isAdmin) {
|
|
128
|
-
throw new Error('Projects with budget over $100,000 require admin approval');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 4. Perform approval
|
|
132
|
-
await api.update('projects', id!, {
|
|
91
|
+
|
|
92
|
+
// Update project to in_progress status
|
|
93
|
+
await api.update(objectName, id, {
|
|
133
94
|
status: 'in_progress',
|
|
134
|
-
approved_by: user?.id,
|
|
95
|
+
approved_by: user?.id || 'system',
|
|
135
96
|
approved_at: new Date(),
|
|
136
|
-
approval_comment: comment
|
|
97
|
+
approval_comment: input.comment
|
|
137
98
|
});
|
|
138
|
-
|
|
139
|
-
// 5. Log approval
|
|
140
|
-
console.log(`[Action] Project ${project.name} approved by ${user?.id}`);
|
|
141
|
-
|
|
99
|
+
|
|
142
100
|
return {
|
|
143
101
|
success: true,
|
|
144
|
-
message:
|
|
102
|
+
message: `Project "${project.name}" approved`,
|
|
145
103
|
new_status: 'in_progress'
|
|
146
104
|
};
|
|
147
105
|
}
|
|
148
106
|
};
|
|
149
107
|
|
|
150
108
|
/**
|
|
151
|
-
*
|
|
109
|
+
* Clone Action
|
|
110
|
+
*
|
|
111
|
+
* Creates a copy of an existing project.
|
|
112
|
+
* Type: Record Action (operates on a single project)
|
|
152
113
|
*/
|
|
153
114
|
interface CloneInput {
|
|
154
115
|
new_name: string;
|
|
155
116
|
copy_tasks?: boolean;
|
|
156
117
|
}
|
|
157
118
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* - Copying related data
|
|
164
|
-
* - Complex multi-step operations
|
|
165
|
-
*/
|
|
166
|
-
export const clone: ActionDefinition<Projects, CloneInput> = {
|
|
167
|
-
handler: async ({ id, input, api, user }) => {
|
|
168
|
-
const { new_name, copy_tasks = false } = input;
|
|
169
|
-
|
|
170
|
-
// 1. Validate input
|
|
171
|
-
if (!new_name || new_name.trim().length === 0) {
|
|
172
|
-
throw new Error('New project name is required');
|
|
119
|
+
export const clone: ActionDefinition<any, CloneInput> = {
|
|
120
|
+
handler: async ({ id, input, api, user, objectName }) => {
|
|
121
|
+
// Validate id is provided
|
|
122
|
+
if (!id) {
|
|
123
|
+
throw new Error('Project ID is required');
|
|
173
124
|
}
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
const sourceProject = await api.findOne(
|
|
177
|
-
|
|
125
|
+
|
|
126
|
+
// Fetch source project
|
|
127
|
+
const sourceProject = await api.findOne(objectName, id);
|
|
178
128
|
if (!sourceProject) {
|
|
179
129
|
throw new Error('Source project not found');
|
|
180
130
|
}
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
name: new_name,
|
|
185
|
-
description:
|
|
186
|
-
status: 'planned', // Always start as planned
|
|
131
|
+
|
|
132
|
+
// Create new project with cloned data
|
|
133
|
+
const newProject = await api.create(objectName, {
|
|
134
|
+
name: input.new_name,
|
|
135
|
+
description: sourceProject.description,
|
|
187
136
|
priority: sourceProject.priority,
|
|
188
|
-
owner: user?.id || sourceProject.owner, // Assign to current user
|
|
189
137
|
budget: sourceProject.budget,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
if (copy_tasks) {
|
|
198
|
-
|
|
199
|
-
const tasks = await api.find('tasks', {
|
|
200
|
-
filters: [['project_id', '=', id]]
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
for (const task of tasks) {
|
|
204
|
-
await api.create('tasks', {
|
|
205
|
-
name: task.name,
|
|
206
|
-
description: task.description,
|
|
207
|
-
project_id: newProject._id,
|
|
208
|
-
status: 'pending', // Reset status
|
|
209
|
-
priority: task.priority
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
*/
|
|
138
|
+
status: 'planned', // Always start cloned projects as planned
|
|
139
|
+
owner: user?.id || 'system', // Assign to current user
|
|
140
|
+
cloned_from: id,
|
|
141
|
+
cloned_at: new Date()
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// TODO: Copy tasks if requested (when tasks functionality is implemented)
|
|
145
|
+
if (input.copy_tasks) {
|
|
146
|
+
// This would copy related tasks
|
|
213
147
|
}
|
|
214
|
-
|
|
215
|
-
console.log(`[Action] Project cloned: ${sourceProject.name} -> ${new_name}`);
|
|
216
|
-
|
|
148
|
+
|
|
217
149
|
return {
|
|
218
150
|
success: true,
|
|
219
|
-
message:
|
|
220
|
-
|
|
221
|
-
new_project_id: newProject._id,
|
|
222
|
-
tasks_copied: copy_tasks
|
|
151
|
+
message: `Project cloned successfully`,
|
|
152
|
+
new_project_id: newProject._id
|
|
223
153
|
};
|
|
224
154
|
}
|
|
225
155
|
};
|
|
226
156
|
|
|
157
|
+
// ===== GLOBAL ACTIONS =====
|
|
158
|
+
// These actions operate on the collection (no specific ID required)
|
|
159
|
+
|
|
227
160
|
/**
|
|
228
|
-
*
|
|
161
|
+
* Import Projects Action
|
|
162
|
+
*
|
|
163
|
+
* Bulk imports projects from external data sources.
|
|
164
|
+
* Type: Global Action (operates on the collection)
|
|
229
165
|
*/
|
|
230
166
|
interface ImportProjectsInput {
|
|
231
|
-
source:
|
|
232
|
-
data
|
|
233
|
-
|
|
167
|
+
source: string;
|
|
168
|
+
data: Array<{
|
|
169
|
+
name?: string;
|
|
170
|
+
description?: string;
|
|
171
|
+
status?: string;
|
|
172
|
+
priority?: string;
|
|
173
|
+
budget?: number;
|
|
174
|
+
}>;
|
|
234
175
|
}
|
|
235
176
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
export const import_projects: ActionDefinition<Projects, ImportProjectsInput> = {
|
|
246
|
-
handler: async ({ input, api, user }) => {
|
|
247
|
-
const { source, data, file_url } = input;
|
|
248
|
-
|
|
249
|
-
console.log(`[Action] Importing projects from ${source} by ${user?.id}`);
|
|
250
|
-
|
|
251
|
-
// 1. Validate input
|
|
252
|
-
if (!data && !file_url) {
|
|
253
|
-
throw new Error('Either data array or file_url must be provided');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 2. Fetch data based on source
|
|
257
|
-
let projectsData: any[] = data || [];
|
|
258
|
-
|
|
259
|
-
if (file_url) {
|
|
260
|
-
// Example: Fetch from URL
|
|
261
|
-
/*
|
|
262
|
-
const response = await fetch(file_url);
|
|
263
|
-
projectsData = await response.json();
|
|
264
|
-
*/
|
|
265
|
-
throw new Error('file_url import not yet implemented');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 3. Validate and import each project
|
|
269
|
-
const results = {
|
|
270
|
-
successCount: 0,
|
|
271
|
-
failed: 0,
|
|
272
|
-
errors: [] as any[]
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
for (let i = 0; i < projectsData.length; i++) {
|
|
276
|
-
const projectData = projectsData[i];
|
|
277
|
-
|
|
177
|
+
export const import_projects: ActionDefinition<any, ImportProjectsInput> = {
|
|
178
|
+
handler: async ({ input, api, user, objectName }) => {
|
|
179
|
+
const errors: Array<{ index: number; error: string }> = [];
|
|
180
|
+
let successCount = 0;
|
|
181
|
+
|
|
182
|
+
// Process each project in the data array
|
|
183
|
+
for (let i = 0; i < input.data.length; i++) {
|
|
184
|
+
const projectData = input.data[i];
|
|
185
|
+
|
|
278
186
|
try {
|
|
279
187
|
// Validate required fields
|
|
280
|
-
if (!projectData.name) {
|
|
188
|
+
if (!projectData.name || projectData.name.trim() === '') {
|
|
281
189
|
throw new Error('Project name is required');
|
|
282
190
|
}
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
// Create project
|
|
296
|
-
await api.create('projects', importData);
|
|
297
|
-
results.successCount++;
|
|
298
|
-
|
|
191
|
+
|
|
192
|
+
// Create the project
|
|
193
|
+
await api.create(objectName, {
|
|
194
|
+
...projectData,
|
|
195
|
+
imported_from: input.source,
|
|
196
|
+
imported_by: user?.id || 'system',
|
|
197
|
+
imported_at: new Date()
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
successCount++;
|
|
299
201
|
} catch (error: any) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
name: projectData.name || 'Unknown',
|
|
304
|
-
error: error.message
|
|
202
|
+
errors.push({
|
|
203
|
+
index: i,
|
|
204
|
+
error: error.message || 'Unknown error'
|
|
305
205
|
});
|
|
306
206
|
}
|
|
307
207
|
}
|
|
308
|
-
|
|
309
|
-
console.log(`[Action] Import completed: ${results.successCount} succeeded, ${results.failed} failed`);
|
|
310
|
-
|
|
208
|
+
|
|
311
209
|
return {
|
|
312
|
-
success:
|
|
313
|
-
message: `Imported ${
|
|
314
|
-
|
|
210
|
+
success: true,
|
|
211
|
+
message: `Imported ${successCount} projects`,
|
|
212
|
+
successCount,
|
|
213
|
+
failed: errors.length,
|
|
214
|
+
errors
|
|
315
215
|
};
|
|
316
216
|
}
|
|
317
217
|
};
|
|
318
218
|
|
|
319
219
|
/**
|
|
320
|
-
*
|
|
220
|
+
* Bulk Update Status Action
|
|
221
|
+
*
|
|
222
|
+
* Updates the status of multiple projects at once.
|
|
223
|
+
* Type: Global Action (operates on multiple records)
|
|
321
224
|
*/
|
|
322
225
|
interface BulkUpdateStatusInput {
|
|
323
226
|
project_ids: string[];
|
|
324
|
-
new_status:
|
|
227
|
+
new_status: string;
|
|
325
228
|
}
|
|
326
229
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
*/
|
|
335
|
-
export const bulk_update_status: ActionDefinition<Projects, BulkUpdateStatusInput> = {
|
|
336
|
-
handler: async ({ input, api, user }) => {
|
|
337
|
-
const { project_ids, new_status } = input;
|
|
338
|
-
|
|
339
|
-
// 1. Validate input
|
|
340
|
-
if (!project_ids || project_ids.length === 0) {
|
|
341
|
-
throw new Error('At least one project ID must be provided');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (project_ids.length > 100) {
|
|
345
|
-
throw new Error('Cannot update more than 100 projects at once');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 2. Update each project
|
|
349
|
-
// Note: This uses an N+1 query pattern (fetching each project individually)
|
|
350
|
-
// For production with large batches, consider fetching all projects at once:
|
|
351
|
-
// const projects = await api.find('projects', { filters: [['_id', 'in', project_ids]] });
|
|
352
|
-
// However, for this example with a 100-project limit, the current approach
|
|
353
|
-
// provides clearer per-record validation and error handling.
|
|
354
|
-
const results = {
|
|
355
|
-
updated: 0,
|
|
356
|
-
skipped: 0,
|
|
357
|
-
errors: [] as any[]
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
for (const id of project_ids) {
|
|
230
|
+
export const bulk_update_status: ActionDefinition<any, BulkUpdateStatusInput> = {
|
|
231
|
+
handler: async ({ input, api, objectName }) => {
|
|
232
|
+
let updated = 0;
|
|
233
|
+
let skipped = 0;
|
|
234
|
+
|
|
235
|
+
// Process each project
|
|
236
|
+
for (const projectId of input.project_ids) {
|
|
361
237
|
try {
|
|
362
|
-
const project = await api.findOne(
|
|
238
|
+
const project = await api.findOne(objectName, projectId);
|
|
363
239
|
|
|
364
240
|
if (!project) {
|
|
365
|
-
|
|
366
|
-
project_id: id,
|
|
367
|
-
error: 'Project not found'
|
|
368
|
-
});
|
|
369
|
-
results.skipped++;
|
|
241
|
+
skipped++;
|
|
370
242
|
continue;
|
|
371
243
|
}
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
if (project.status === 'completed'
|
|
375
|
-
|
|
376
|
-
project_id: id,
|
|
377
|
-
name: project.name,
|
|
378
|
-
error: 'Cannot change status of completed projects'
|
|
379
|
-
});
|
|
380
|
-
results.skipped++;
|
|
244
|
+
|
|
245
|
+
// Skip completed projects (they cannot be changed)
|
|
246
|
+
if (project.status === 'completed') {
|
|
247
|
+
skipped++;
|
|
381
248
|
continue;
|
|
382
249
|
}
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
await api.update(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
} catch (error: any) {
|
|
389
|
-
results.errors.push({
|
|
390
|
-
project_id: id,
|
|
391
|
-
error: error.message
|
|
250
|
+
|
|
251
|
+
// Update the status
|
|
252
|
+
await api.update(objectName, projectId, {
|
|
253
|
+
status: input.new_status
|
|
392
254
|
});
|
|
393
|
-
|
|
255
|
+
|
|
256
|
+
updated++;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
skipped++;
|
|
394
259
|
}
|
|
395
260
|
}
|
|
396
|
-
|
|
397
|
-
console.log(`[Action] Bulk update: ${results.updated} updated, ${results.skipped} skipped`);
|
|
398
|
-
|
|
261
|
+
|
|
399
262
|
return {
|
|
400
|
-
success:
|
|
401
|
-
message: `Updated ${
|
|
402
|
-
|
|
263
|
+
success: true,
|
|
264
|
+
message: `Updated ${updated} projects`,
|
|
265
|
+
updated,
|
|
266
|
+
skipped
|
|
403
267
|
};
|
|
404
268
|
}
|
|
405
269
|
};
|
|
406
270
|
|
|
407
271
|
/**
|
|
408
|
-
*
|
|
272
|
+
* Generate Report Action
|
|
409
273
|
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* - Data collection across records
|
|
413
|
-
* - Computed results
|
|
274
|
+
* Generates statistical reports about projects.
|
|
275
|
+
* Type: Global Action (analytics on the collection)
|
|
414
276
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
277
|
+
interface GenerateReportInput {
|
|
278
|
+
// Optional filters could be added here
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const generate_report: ActionDefinition<any, GenerateReportInput> = {
|
|
282
|
+
handler: async ({ api, objectName }) => {
|
|
283
|
+
// Fetch all projects
|
|
284
|
+
const projects = await api.find(objectName, {});
|
|
285
|
+
|
|
286
|
+
// Calculate statistics
|
|
423
287
|
const report = {
|
|
424
|
-
total_projects:
|
|
288
|
+
total_projects: projects.length,
|
|
425
289
|
by_status: {
|
|
426
290
|
planned: 0,
|
|
427
291
|
in_progress: 0,
|
|
428
292
|
completed: 0
|
|
429
|
-
},
|
|
430
|
-
by_priority: {
|
|
431
|
-
low: 0,
|
|
432
|
-
normal: 0,
|
|
433
|
-
high: 0
|
|
434
|
-
},
|
|
293
|
+
} as Record<string, number>,
|
|
294
|
+
by_priority: {} as Record<string, number>,
|
|
435
295
|
total_budget: 0,
|
|
436
|
-
average_budget: 0
|
|
437
|
-
generated_at: new Date(),
|
|
438
|
-
generated_by: user?.id
|
|
296
|
+
average_budget: 0
|
|
439
297
|
};
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
|
|
298
|
+
|
|
299
|
+
// Aggregate data
|
|
300
|
+
projects.forEach((project: any) => {
|
|
443
301
|
// Count by status
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
report.by_status[status as keyof typeof report.by_status]++;
|
|
302
|
+
if (project.status) {
|
|
303
|
+
report.by_status[project.status] = (report.by_status[project.status] || 0) + 1;
|
|
447
304
|
}
|
|
448
|
-
|
|
305
|
+
|
|
449
306
|
// Count by priority
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
307
|
+
if (project.priority) {
|
|
308
|
+
report.by_priority[project.priority] = (report.by_priority[project.priority] || 0) + 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Sum budgets
|
|
312
|
+
if (project.budget) {
|
|
313
|
+
report.total_budget += project.budget;
|
|
453
314
|
}
|
|
454
|
-
|
|
455
|
-
// Sum budget
|
|
456
|
-
report.total_budget += project.budget || 0;
|
|
457
315
|
});
|
|
458
|
-
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
console.log(`[Action] Report generated: ${report.total_projects} projects analyzed`);
|
|
465
|
-
|
|
316
|
+
|
|
317
|
+
// Calculate average budget
|
|
318
|
+
if (projects.length > 0) {
|
|
319
|
+
report.average_budget = report.total_budget / projects.length;
|
|
320
|
+
}
|
|
321
|
+
|
|
466
322
|
return {
|
|
467
323
|
success: true,
|
|
468
324
|
message: 'Report generated successfully',
|
|
469
|
-
report
|
|
325
|
+
report,
|
|
326
|
+
generated_at: new Date()
|
|
470
327
|
};
|
|
471
328
|
}
|
|
472
329
|
};
|