@masterteam/task-schedule 0.0.22 → 0.0.24

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/README.md CHANGED
@@ -26,10 +26,10 @@ Main context fields:
26
26
  - `dateFormat`: default `dd/MM/yyyy`
27
27
  - `height`: default `760px`
28
28
  - `pdfFonts`: optional `{ regular?: string; arabic?: string; size?: number }`
29
- - `mppRequestTimeoutMs`: optional timeout for MPP import, apply, and export requests. Defaults to `300000` ms.
30
- - `mppPreviewMaxRows`: optional MPP import normalization cap. Defaults to `400` preview rows.
31
- - `mppPreviewMaxResponseBytes`: optional maximum import response size to parse for preview rows. Defaults to `5000000` bytes; larger successful responses are treated as direct backend-applied imports.
32
- - `mppKeepRawPayload`: optional flag to retain the raw MPP import response on `TaskScheduleImportResult.raw`. Defaults to `false`.
29
+ - `mppRequestTimeoutMs`: optional timeout for MPP import, apply, and export requests. Defaults to `300000` ms.
30
+ - `mppPreviewMaxRows`: optional MPP import normalization cap. Defaults to `400` preview rows.
31
+ - `mppPreviewMaxResponseBytes`: optional maximum import response size to parse for preview rows. Defaults to `5000000` bytes; larger successful responses are treated as direct backend-applied imports.
32
+ - `mppKeepRawPayload`: optional flag to retain the raw MPP import response on `TaskScheduleImportResult.raw`. Defaults to `false`.
33
33
 
34
34
  ## Outputs
35
35
 
@@ -108,16 +108,17 @@ Backend routes used:
108
108
  - Custom view config: `GET /api/schedulemanager/{levelId}/schedule/{customViewId}`
109
109
  - Team members (edit/resources mode): `GET /api/levels/{levelId}/TeamMember`
110
110
 
111
- Runtime mutations (when `levelDataId` exists):
112
-
113
- - `POST /api/process-submit` for create/update/delete/progress
114
- - `PUT /api/levels/{levelId}/{levelDataId}/schedule/bulk-update`
115
- - `PUT /api/levels/{levelId}/{levelDataId}/schedule/reorder`
111
+ Runtime mutations (when `levelDataId` exists):
112
+
113
+ - `POST /api/process-submit` for create/update/delete/progress
114
+ - `PUT /api/levels/{levelId}/{levelDataId}/schedule/bulk-update`
115
+ - `PUT /api/levels/{levelId}/{levelDataId}/schedule/reorder`
116
116
  - `POST /api/levels/{levelId}/{levelDataId}/schedule/baselines` (set baseline)
117
117
  - `GET /api/levels/{levelId}/{levelDataId}/schedule/mpp` (export)
118
-
119
- Plain schedule reads no longer use legacy `/api/tasks/...` read routes.
120
- Import dialog keeps its current MPP preview/apply flow unless `endpoints.importTasks` / `endpoints.applyImportedTasks` are overridden.
118
+
119
+ Plain schedule reads no longer use legacy `/api/tasks/...` read routes.
120
+ Import dialog keeps its current MPP preview/apply flow unless `endpoints.importTasks` / `endpoints.applyImportedTasks` are overridden.
121
+ Row drag/drop reorder is optimistic: the component sends the parent/order write requests and leaves the current Gantt view in place instead of immediately reloading the schedule.
121
122
 
122
123
  For `schedule/read` query responses, `TaskScheduleFetchService` maps native fields through `catalog.properties`: it resolves each property's `normalizedKey` to the Gantt field and reads the matching `record.values[property.key]` wrapper. Date fields prefer `raw` values and are converted from valid date/UTC strings to local date-only `Date` objects before binding to Syncfusion Gantt.
123
124
 
@@ -141,11 +142,11 @@ When no PDF font is provided, package fallback uses embedded `TASK_SCHEDULE_DEFA
141
142
 
142
143
  ## Import Dialog
143
144
 
144
- Use `TaskScheduleImportDialog` with `ModalService` to import `.mpp`/`.xer` files and apply tasks (replace or append).
145
- The dialog is Tailwind-based and uses `@masterteam/components` (`mt-button`, `mt-entity-preview`).
146
- Imported rows render as a checkbox tree so nested MPP summary tasks and children stay readable before apply.
147
- If the MPP import endpoint returns a successful empty body or a response larger than `mppPreviewMaxResponseBytes`, the package treats it as a direct backend-applied import, closes the dialog with a success result, and lets the host schedule reload instead of rendering an empty preview.
148
- The preview is capped by `maxRows`/`context.mppPreviewMaxRows` to keep large MPP files responsive. Select-all and apply operate on the visible preview rows only when the backend returns more rows than the preview cap. The full raw import payload is not retained unless `context.mppKeepRawPayload` is enabled.
145
+ Use `TaskScheduleImportDialog` with `ModalService` to import `.mpp`/`.xer` files and apply tasks (replace or append).
146
+ The dialog is Tailwind-based and uses `@masterteam/components` (`mt-button`, `mt-entity-preview`).
147
+ Imported rows render as a checkbox tree so nested MPP summary tasks and children stay readable before apply.
148
+ If the MPP import endpoint returns a successful empty body or a response larger than `mppPreviewMaxResponseBytes`, the package treats it as a direct backend-applied import, closes the dialog with a success result, and lets the host schedule reload instead of rendering an empty preview.
149
+ The preview is capped by `maxRows`/`context.mppPreviewMaxRows` to keep large MPP files responsive. Select-all and apply operate on the visible preview rows only when the backend returns more rows than the preview cap. The full raw import payload is not retained unless `context.mppKeepRawPayload` is enabled.
149
150
 
150
151
  ## Shell Component
151
152
 
@@ -1280,7 +1280,6 @@ function normalizeLookupKey(value) {
1280
1280
  .replace(/[^a-z0-9]/g, '');
1281
1281
  }
1282
1282
 
1283
- const DEFAULT_TASK_MODULE_ID = 3;
1284
1283
  const DEFAULT_MPP_REQUEST_TIMEOUT_MS = 300_000;
1285
1284
  class TaskScheduleActionService {
1286
1285
  http = inject(HttpClient);
@@ -1507,9 +1506,14 @@ class TaskScheduleActionService {
1507
1506
  return this.submitTaskMutation(context, 'UpdateProgress', payload, taskId);
1508
1507
  }
1509
1508
  applyImportedTasks(context, payload) {
1509
+ // Submit/import the reviewed preview tasks. The level-scoped route is the
1510
+ // preferred contract (Pplus4-style split flow): upload/parse only previews,
1511
+ // this PATCH saves. The old `tasks/{levelDataId}/import` shape fed levelId
1512
+ // into a levelDataId slot — wrong record — so imports never saved correctly.
1510
1513
  const endpoint = this.interpolateEndpointTemplate(this.readEndpoint(context, 'applyImportedTasks') ??
1511
- 'tasks/{levelId}/import', {
1514
+ 'levels/{levelId}/{levelDataId}/tasks/import', {
1512
1515
  levelId: this.requireId(context.levelId, 'Apply import requires levelId.'),
1516
+ levelDataId: this.requireId(context.levelDataId, 'Apply import requires levelDataId.'),
1513
1517
  });
1514
1518
  return this.readData(this.http
1515
1519
  .patch(endpoint, payload)
@@ -1521,9 +1525,17 @@ class TaskScheduleActionService {
1521
1525
  operationKey: 'SetBaseline',
1522
1526
  levelId: this.requireProcessId(context.levelId),
1523
1527
  levelDataId: this.requireProcessId(context.levelDataId),
1524
- moduleId: context.taskModuleId ?? DEFAULT_TASK_MODULE_ID,
1525
1528
  fields: [],
1526
1529
  };
1530
+ // The backend resolves the module from `moduleKey` — the task
1531
+ // create/update/delete/progress mutations (buildProcessSubmitRequest) all
1532
+ // omit moduleId and work. SetBaseline used to force a hardcoded moduleId
1533
+ // (was DEFAULT_TASK_MODULE_ID = 3), which targets the wrong module on any
1534
+ // template whose Task module isn't id 3 → "Record '<levelDataId>' was not
1535
+ // found". Only forward an explicitly-provided override.
1536
+ if (context.taskModuleId !== undefined && context.taskModuleId !== null) {
1537
+ request.moduleId = context.taskModuleId;
1538
+ }
1527
1539
  const endpoint = this.interpolateEndpointTemplate(this.readEndpoint(context, 'processSubmit') ?? 'process-submit', {});
1528
1540
  return this.readData(this.http.post(endpoint, request));
1529
1541
  }
@@ -2641,15 +2653,24 @@ class TaskScheduleDialog {
2641
2653
  this.ref.close(response);
2642
2654
  }
2643
2655
  mapSubmitRequest(request) {
2644
- if (this.mode() !== 'create' || !this.parentGuid()) {
2645
- return request;
2656
+ // Schedule items submit under their type-specific schedule module key
2657
+ // ('Task' | 'Milestone' | 'Deliverable'), not the generic 'ModuleData' the
2658
+ // form loads with. The backend only maps the task_* fields (and the parent
2659
+ // link) when the schedule module key is used.
2660
+ const moduleKey = this.selectedType() ?? request.moduleKey;
2661
+ const isSubtaskCreate = this.mode() === 'create' && !!this.parentGuid();
2662
+ if (!isSubtaskCreate) {
2663
+ return { ...request, moduleKey };
2646
2664
  }
2647
2665
  return {
2648
2666
  ...request,
2667
+ moduleKey,
2649
2668
  fields: [
2650
2669
  ...(request.fields ?? []),
2651
2670
  {
2652
- propertyKey: 'parentId',
2671
+ // Parent reference is the anchor row's schedule guid / externalId,
2672
+ // not its numeric database id.
2673
+ propertyKey: 'externalParentId',
2653
2674
  value: this.parentGuid(),
2654
2675
  },
2655
2676
  ],
@@ -3124,12 +3145,12 @@ class TaskScheduleImportDialog {
3124
3145
  subtasks: [],
3125
3146
  children: [],
3126
3147
  };
3127
- payloadTask['startDate'] = this.toApiDate(task.startDate);
3128
- payloadTask['finishDate'] = this.toApiDate(task.finishDate);
3129
- payloadTask['baselineStart'] = this.toApiDate(task.baselineStartDate ?? task.baselineStart);
3130
- payloadTask['baselineFinish'] = this.toApiDate(task.baselineEndDate ?? task.baselineFinish);
3131
- payloadTask['actualStart'] = this.toApiDate(task.actualStartDate ?? task.actualStart);
3132
- payloadTask['actualFinish'] = this.toApiDate(task.actualFinishDate ?? task.actualFinish);
3148
+ payloadTask['startDate'] = this.toApiDateTime(task.startDate);
3149
+ payloadTask['finishDate'] = this.toApiDateTime(task.finishDate);
3150
+ payloadTask['baselineStart'] = this.toApiDateTime(task.baselineStartDate ?? task.baselineStart);
3151
+ payloadTask['baselineFinish'] = this.toApiDateTime(task.baselineEndDate ?? task.baselineFinish);
3152
+ payloadTask['actualStart'] = this.toApiDateTime(task.actualStartDate ?? task.actualStart);
3153
+ payloadTask['actualFinish'] = this.toApiDateTime(task.actualFinishDate ?? task.actualFinish);
3133
3154
  payloadTask['customProperties'] =
3134
3155
  task.customProperties ?? task.props ?? [];
3135
3156
  payloadTask['isMilestone'] =
@@ -3327,6 +3348,21 @@ class TaskScheduleImportDialog {
3327
3348
  const day = String(date.getDate()).padStart(2, '0');
3328
3349
  return `${year}-${month}-${day}`;
3329
3350
  }
3351
+ /**
3352
+ * Submit/import payload dates must be ISO 8601 UTC timestamps ending with
3353
+ * 'Z' (backend rejects bare 'YYYY-MM-DD' with VAL_001). `toApiDate` yields a
3354
+ * calendar day for display/editing; this widens it to a full UTC instant for
3355
+ * the import call. Date-only strings parse as UTC midnight, so the calendar
3356
+ * day is preserved (no timezone shift).
3357
+ */
3358
+ toApiDateTime(value) {
3359
+ const normalized = this.toApiDate(value);
3360
+ if (!normalized) {
3361
+ return null;
3362
+ }
3363
+ const date = new Date(normalized);
3364
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
3365
+ }
3330
3366
  countTasks(tasks, maxCount) {
3331
3367
  let count = 0;
3332
3368
  const walk = (rows) => {
@@ -3940,25 +3976,22 @@ class TaskSchedule {
3940
3976
  const { taskId, parentId } = rowDropUpdate;
3941
3977
  const sub = this.actionService
3942
3978
  .updateParent(context, taskId, parentId)
3943
- .pipe(finalize(() => {
3944
- const payload = this.actionService.buildOrderPayload(this.ganttObj?.flatData);
3945
- if (!payload.length) {
3946
- this.reloadSchedule();
3947
- return;
3948
- }
3949
- const orderSub = this.actionService
3950
- .updateOrder(context, payload)
3951
- .subscribe({
3952
- next: () => this.reloadSchedule(),
3953
- error: () => this.reloadSchedule(),
3954
- });
3955
- this.operationSub.add(orderSub);
3956
- }))
3957
3979
  .subscribe({
3958
- error: () => this.reloadSchedule(),
3980
+ next: () => this.persistCurrentOrder(context),
3981
+ error: (error) => this.emitActionError(this.getErrorMessage(error)),
3959
3982
  });
3960
3983
  this.operationSub.add(sub);
3961
3984
  }
3985
+ persistCurrentOrder(context) {
3986
+ const payload = this.actionService.buildOrderPayload(this.ganttObj?.flatData);
3987
+ if (!payload.length) {
3988
+ return;
3989
+ }
3990
+ const orderSub = this.actionService.updateOrder(context, payload).subscribe({
3991
+ error: (error) => this.emitActionError(this.getErrorMessage(error)),
3992
+ });
3993
+ this.operationSub.add(orderSub);
3994
+ }
3962
3995
  updateTask(args) {
3963
3996
  const context = this.context();
3964
3997
  if (!context) {