@masterteam/task-schedule 0.0.23 → 0.0.25

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
  }
@@ -2665,7 +2677,7 @@ class TaskScheduleDialog {
2665
2677
  };
2666
2678
  }
2667
2679
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TaskScheduleDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
2668
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: TaskScheduleDialog, isStandalone: true, selector: "mt-task-schedule-dialog", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, typeOptions: { classPropertyName: "typeOptions", publicName: "typeOptions", isSignal: true, isRequired: false, transformFunction: null }, langCode: { classPropertyName: "langCode", publicName: "langCode", isSignal: true, isRequired: false, transformFunction: null }, parentGuid: { classPropertyName: "parentGuid", publicName: "parentGuid", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "clientForm", first: true, predicate: ClientForm, descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"'p-4 overflow-y-auto ' + (modal.contentClass || '')\">\r\n <div class=\"flex flex-col gap-4\">\r\n <mt-select-field\r\n [options]=\"typeSelectOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [label]=\"'task-schedule.dialog.type' | transloco\"\r\n [placeholder]=\"'task-schedule.dialog.typePlaceholder' | transloco\"\r\n [ngModel]=\"selectedType() ?? undefined\"\r\n (onChange)=\"onTypeChange($event)\"\r\n />\r\n\r\n @if (selectedType()) {\r\n <mt-client-form\r\n #clientForm\r\n [moduleKey]=\"'ModuleData'\"\r\n [moduleId]=\"selectedModuleId() ?? undefined\"\r\n [operationKey]=\"mode() === 'edit' ? 'Update' : 'Create'\"\r\n [formMode]=\"mode()\"\r\n [levelId]=\"context()?.levelId ?? undefined\"\r\n [levelDataId]=\"context()?.levelDataId ?? undefined\"\r\n [moduleDataId]=\"selectedModuleDataId() ?? undefined\"\r\n [lang]=\"langCode()\"\r\n [autoLoad]=\"true\"\r\n [ignoredFieldKeys]=\"ignoredFieldKeys()\"\r\n [submitRequestMapper]=\"submitRequestMapper\"\r\n (submitted)=\"onClientFormSubmitted($event)\"\r\n />\r\n } @else {\r\n <div\r\n class=\"rounded-xl border border-surface bg-content px-4 py-4 text-sm text-muted-color\"\r\n >\r\n {{ \"task-schedule.dialog.typeFirst\" | transloco }}\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<div [class]=\"modal.footerClass\">\r\n <div class=\"flex items-center justify-end gap-3\">\r\n <mt-button\r\n [label]=\"'task-schedule.dialog.cancel' | transloco\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"'task-schedule.dialog.save' | transloco\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: ClientForm, selector: "mt-client-form", inputs: ["moduleKey", "operationKey", "moduleId", "levelId", "levelDataId", "moduleDataId", "requestSchemaId", "draftProcessId", "preview", "returnUrl", "defaultValues", "submitRequestMapper", "readonly", "autoLoad", "formMode", "renderMode", "showInternalStepActions", "confirmWarningsOnSubmit", "confirmWarningsOnStepChange", "readonlyFieldDisplayMode", "lookups", "statuses", "ignoredFieldKeys", "allowedFieldKeys"], outputs: ["loaded", "submitted", "errored", "modeDetected", "formSourceDetected", "footerStateChanged"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
2680
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: TaskScheduleDialog, isStandalone: true, selector: "mt-task-schedule-dialog", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, typeOptions: { classPropertyName: "typeOptions", publicName: "typeOptions", isSignal: true, isRequired: false, transformFunction: null }, langCode: { classPropertyName: "langCode", publicName: "langCode", isSignal: true, isRequired: false, transformFunction: null }, parentGuid: { classPropertyName: "parentGuid", publicName: "parentGuid", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "clientForm", first: true, predicate: ClientForm, descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"'p-4 overflow-y-auto ' + (modal.contentClass || '')\">\r\n <div class=\"flex flex-col gap-4\">\r\n <mt-select-field\r\n [options]=\"typeSelectOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [label]=\"'task-schedule.dialog.type' | transloco\"\r\n [placeholder]=\"'task-schedule.dialog.typePlaceholder' | transloco\"\r\n [ngModel]=\"selectedType() ?? undefined\"\r\n (onChange)=\"onTypeChange($event)\"\r\n />\r\n\r\n @if (selectedType()) {\r\n <mt-client-form\r\n #clientForm\r\n [moduleKey]=\"'ModuleData'\"\r\n [moduleId]=\"selectedModuleId() ?? undefined\"\r\n [operationKey]=\"mode() === 'edit' ? 'Update' : 'Create'\"\r\n [formMode]=\"mode()\"\r\n [levelId]=\"context()?.levelId ?? undefined\"\r\n [levelDataId]=\"context()?.levelDataId ?? undefined\"\r\n [moduleDataId]=\"selectedModuleDataId() ?? undefined\"\r\n [lang]=\"langCode()\"\r\n [autoLoad]=\"true\"\r\n [ignoredFieldKeys]=\"ignoredFieldKeys()\"\r\n [submitRequestMapper]=\"submitRequestMapper\"\r\n (submitted)=\"onClientFormSubmitted($event)\"\r\n />\r\n } @else {\r\n <div\r\n class=\"rounded-xl border border-surface bg-content px-4 py-4 text-sm text-muted-color\"\r\n >\r\n {{ \"task-schedule.dialog.typeFirst\" | transloco }}\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<div [class]=\"modal.footerClass\">\r\n <div class=\"flex items-center justify-end gap-3\">\r\n <mt-button\r\n [label]=\"'task-schedule.dialog.cancel' | transloco\"\r\n variant=\"outlined\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"'task-schedule.dialog.save' | transloco\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: ClientForm, selector: "mt-client-form", inputs: ["moduleKey", "operationKey", "moduleId", "levelId", "levelDataId", "moduleDataId", "requestSchemaId", "draftProcessId", "preview", "forceOriginalForm", "returnUrl", "defaultValues", "submitRequestMapper", "readonly", "autoLoad", "formMode", "renderMode", "showInternalStepActions", "confirmWarningsOnSubmit", "confirmWarningsOnStepChange", "readonlyFieldDisplayMode", "lookups", "statuses", "ignoredFieldKeys", "allowedFieldKeys"], outputs: ["loaded", "submitted", "errored", "modeDetected", "formSourceDetected", "footerStateChanged"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
2669
2681
  }
2670
2682
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TaskScheduleDialog, decorators: [{
2671
2683
  type: Component,
@@ -3123,29 +3135,36 @@ class TaskScheduleImportDialog {
3123
3135
  }
3124
3136
  buildApplyPayloadTask(row) {
3125
3137
  const task = row.task;
3138
+ // The MPP parse leaves externalId as "None" for tasks without one; the
3139
+ // import DTO rejects "None" and requires unique externalIds, expressing
3140
+ // hierarchy via externalParentId (parentGuid is ignored). Send the task's
3141
+ // stable unique id as externalId and link children to the parent's id.
3142
+ const externalParentId = this.resolveParentExternalIdForPreviewRow(row.key);
3126
3143
  const payloadTask = {
3127
3144
  ...task,
3128
3145
  name: String(task.title ?? task.name ?? ''),
3129
3146
  predecessors: task.predecessor ?? task.predecessors ?? '',
3130
- parentGuid: task.parentGuid ?? this.resolveParentGuidForPreviewRow(row.key),
3147
+ parentGuid: externalParentId,
3131
3148
  assignedTo: this.resolveAssignedTo(task),
3132
3149
  isSelected: true,
3133
3150
  subtasks: [],
3134
3151
  children: [],
3135
3152
  };
3136
- payloadTask['startDate'] = this.toApiDate(task.startDate);
3137
- payloadTask['finishDate'] = this.toApiDate(task.finishDate);
3138
- payloadTask['baselineStart'] = this.toApiDate(task.baselineStartDate ?? task.baselineStart);
3139
- payloadTask['baselineFinish'] = this.toApiDate(task.baselineEndDate ?? task.baselineFinish);
3140
- payloadTask['actualStart'] = this.toApiDate(task.actualStartDate ?? task.actualStart);
3141
- payloadTask['actualFinish'] = this.toApiDate(task.actualFinishDate ?? task.actualFinish);
3153
+ payloadTask['externalId'] = this.toStableExternalId(task);
3154
+ payloadTask['externalParentId'] = externalParentId;
3155
+ payloadTask['startDate'] = this.toApiDateTime(task.startDate);
3156
+ payloadTask['finishDate'] = this.toApiDateTime(task.finishDate);
3157
+ payloadTask['baselineStart'] = this.toApiDateTime(task.baselineStartDate ?? task.baselineStart);
3158
+ payloadTask['baselineFinish'] = this.toApiDateTime(task.baselineEndDate ?? task.baselineFinish);
3159
+ payloadTask['actualStart'] = this.toApiDateTime(task.actualStartDate ?? task.actualStart);
3160
+ payloadTask['actualFinish'] = this.toApiDateTime(task.actualFinishDate ?? task.actualFinish);
3142
3161
  payloadTask['customProperties'] =
3143
3162
  task.customProperties ?? task.props ?? [];
3144
3163
  payloadTask['isMilestone'] =
3145
3164
  String(task.type ?? task.typeLabel ?? task.typeLable ?? '').toLowerCase() === 'milestone';
3146
3165
  return payloadTask;
3147
3166
  }
3148
- resolveParentGuidForPreviewRow(rowKey) {
3167
+ resolveParentExternalIdForPreviewRow(rowKey) {
3149
3168
  const path = this.readPathFromRowKey(rowKey);
3150
3169
  if (!path) {
3151
3170
  return null;
@@ -3155,13 +3174,29 @@ class TaskScheduleImportDialog {
3155
3174
  return null;
3156
3175
  }
3157
3176
  const parent = this.findTaskByPath(steps.slice(0, -1).join('.'));
3158
- if (!parent) {
3159
- return null;
3177
+ return parent ? this.toStableExternalId(parent) : null;
3178
+ }
3179
+ /**
3180
+ * A unique, stable external id for the import DTO. The MPP parse yields
3181
+ * `guid`/`externalId` of "None" for tasks without one, which the backend
3182
+ * rejects as a duplicate/reserved value — fall back to the task's unique id.
3183
+ */
3184
+ toStableExternalId(task) {
3185
+ const candidates = [
3186
+ task.guid,
3187
+ task['externalId'],
3188
+ task.id,
3189
+ ];
3190
+ for (const candidate of candidates) {
3191
+ if (candidate === null || candidate === undefined) {
3192
+ continue;
3193
+ }
3194
+ const value = String(candidate).trim();
3195
+ if (value && value.toLowerCase() !== 'none') {
3196
+ return value;
3197
+ }
3160
3198
  }
3161
- const parentGuid = parent.guid ?? parent.id ?? null;
3162
- return parentGuid === null || parentGuid === undefined
3163
- ? null
3164
- : String(parentGuid);
3199
+ return null;
3165
3200
  }
3166
3201
  resolveChildTasks(task) {
3167
3202
  const subtasks = Array.isArray(task.subtasks) ? task.subtasks : [];
@@ -3336,6 +3371,21 @@ class TaskScheduleImportDialog {
3336
3371
  const day = String(date.getDate()).padStart(2, '0');
3337
3372
  return `${year}-${month}-${day}`;
3338
3373
  }
3374
+ /**
3375
+ * Submit/import payload dates must be ISO 8601 UTC timestamps ending with
3376
+ * 'Z' (backend rejects bare 'YYYY-MM-DD' with VAL_001). `toApiDate` yields a
3377
+ * calendar day for display/editing; this widens it to a full UTC instant for
3378
+ * the import call. Date-only strings parse as UTC midnight, so the calendar
3379
+ * day is preserved (no timezone shift).
3380
+ */
3381
+ toApiDateTime(value) {
3382
+ const normalized = this.toApiDate(value);
3383
+ if (!normalized) {
3384
+ return null;
3385
+ }
3386
+ const date = new Date(normalized);
3387
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
3388
+ }
3339
3389
  countTasks(tasks, maxCount) {
3340
3390
  let count = 0;
3341
3391
  const walk = (rows) => {
@@ -3949,25 +3999,22 @@ class TaskSchedule {
3949
3999
  const { taskId, parentId } = rowDropUpdate;
3950
4000
  const sub = this.actionService
3951
4001
  .updateParent(context, taskId, parentId)
3952
- .pipe(finalize(() => {
3953
- const payload = this.actionService.buildOrderPayload(this.ganttObj?.flatData);
3954
- if (!payload.length) {
3955
- this.reloadSchedule();
3956
- return;
3957
- }
3958
- const orderSub = this.actionService
3959
- .updateOrder(context, payload)
3960
- .subscribe({
3961
- next: () => this.reloadSchedule(),
3962
- error: () => this.reloadSchedule(),
3963
- });
3964
- this.operationSub.add(orderSub);
3965
- }))
3966
4002
  .subscribe({
3967
- error: () => this.reloadSchedule(),
4003
+ next: () => this.persistCurrentOrder(context),
4004
+ error: (error) => this.emitActionError(this.getErrorMessage(error)),
3968
4005
  });
3969
4006
  this.operationSub.add(sub);
3970
4007
  }
4008
+ persistCurrentOrder(context) {
4009
+ const payload = this.actionService.buildOrderPayload(this.ganttObj?.flatData);
4010
+ if (!payload.length) {
4011
+ return;
4012
+ }
4013
+ const orderSub = this.actionService.updateOrder(context, payload).subscribe({
4014
+ error: (error) => this.emitActionError(this.getErrorMessage(error)),
4015
+ });
4016
+ this.operationSub.add(orderSub);
4017
+ }
3971
4018
  updateTask(args) {
3972
4019
  const context = this.context();
3973
4020
  if (!context) {