@outbuild-company/schedule-core 1.0.0 → 1.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/index.js CHANGED
@@ -16169,132 +16169,241 @@ var WORKING_DAY_TRUE_VALUES = /* @__PURE__ */ new Set([
16169
16169
  "true"
16170
16170
  ]);
16171
16171
  var DEFAULT_ACTIVITY_CRETER = "DURATION";
16172
- var VALID_ACTIVITY_CRETER_VALUES = /* @__PURE__ */ new Set(["DURATION", "COST", "HH"]);
16173
16172
  var DEFAULT_CONSTRAINT_TYPE = "asap";
16173
+ var CONSTRAINT_LABEL_MAP = {
16174
+ "AS SOON AS POSSIBLE": "asap",
16175
+ "AS LATE AS POSSIBLE": "alap",
16176
+ "START NO EARLIER THAN": "snet",
16177
+ "START NO LATER THAN": "snlt",
16178
+ "FINISH NO EARLIER THAN": "fnet",
16179
+ "FINISH NO LATER THAN": "fnlt",
16180
+ "MUST START ON": "mso",
16181
+ "MUST FINISH ON": "mfo"
16182
+ };
16174
16183
 
16175
- // src/boundary/backend/parse-from-backend.ts
16176
- function parseFromBackend(input, reporter = noopReporter) {
16177
- const sector = parseSector(input.sector);
16178
- const rawCalendars = (input.calendars && input.calendars.length > 0 ? input.calendars : input.sector.calendars) ?? [];
16179
- const calendars = rawCalendars.map(parseCalendar);
16180
- const calendarIds = new Set(calendars.map((c) => c.id));
16181
- const defaultCalendarId = pickDefaultCalendarId(calendars);
16182
- const baseCalendars = (input.baseCalendars ?? []).map(parseBaseCalendar);
16183
- const activities = input.activities.map((a) => {
16184
- const parsed2 = parseActivity(a, calendarIds, defaultCalendarId, reporter);
16185
- parsed2.for_disable_milestone_duration = Math.round(
16186
- Number(parsed2.duration) * sector.hoursPerDay
16184
+ // src/boundary/backend/normalize-activity.ts
16185
+ function normalizeActivities(rawActivities, context) {
16186
+ const activities = rawActivities.map((raw) => parseActivity(raw, context));
16187
+ forceRootProjectToRoot(activities);
16188
+ const activityIds = new Set(activities.map((activity) => activity.id));
16189
+ reparentOrphansToRoot(activities, activityIds, context.reporter);
16190
+ return { activities, activityIds };
16191
+ }
16192
+ function proplannerIdField(raw) {
16193
+ return typeof raw.id === "number" ? { proplannerId: raw.id } : {};
16194
+ }
16195
+ function isAutoSchedulable(progress) {
16196
+ return progress === 0;
16197
+ }
16198
+ function parseConstraintLabel(label, reporter) {
16199
+ if (!label) return null;
16200
+ const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
16201
+ if (normalized === void 0) {
16202
+ reporter.warn(
16203
+ `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
16187
16204
  );
16188
- return parsed2;
16189
- });
16190
- const rootProject = activities.find(
16191
- (a) => a.correlative_id === 0 && a.type === "project"
16192
- );
16193
- if (rootProject) rootProject.parent = ROOT_PARENT_ID;
16194
- const activityIds = new Set(activities.map((a) => a.id));
16195
- reparentOrphansToRoot(activities, activityIds, reporter);
16196
- const links = parseLinksDroppingDanglingReferences(
16197
- input.links,
16198
- activityIds,
16199
- sector.hoursPerDay,
16200
- reporter
16201
- );
16202
- return { sector, activities, links, calendars, baseCalendars };
16205
+ return DEFAULT_CONSTRAINT_TYPE;
16206
+ }
16207
+ return normalized;
16203
16208
  }
16204
- function parseBaseCalendar(raw) {
16205
- const r = raw;
16206
- const reshaped = {
16207
- ...raw,
16208
- exceptiondays: r.baseexceptiondays ?? raw.exceptiondays ?? [],
16209
- id: `${raw.id}-base`,
16210
- baseDefault: Boolean(raw.is_default),
16211
- is_default: false
16209
+ function resolveConstraint(raw, reporter) {
16210
+ const constraintType = parseConstraintLabel(raw.constraint, reporter);
16211
+ const constraintTakesDate = constraintType !== "asap" && constraintType !== "alap";
16212
+ const constraintDate = constraintTakesDate && raw.constraint_date ? parseBackendDate(raw.constraint_date) : null;
16213
+ return { constraint_type: constraintType, constraint_date: constraintDate };
16214
+ }
16215
+ function resolveDatesAndDuration(raw) {
16216
+ const startDate = parseBackendDate(raw.start_date);
16217
+ const isMilestone = raw.type === "milestone";
16218
+ return {
16219
+ start_date: startDate,
16220
+ end_date: isMilestone ? startDate : parseBackendDate(raw.end_date),
16221
+ duration: isMilestone ? 0 : Number(raw.duration)
16212
16222
  };
16213
- return parseCalendar(reshaped);
16214
16223
  }
16215
- function pickDefaultCalendarId(calendars) {
16216
- const flaggedDefault = calendars.find((cal) => cal.is_default);
16217
- if (flaggedDefault) return flaggedDefault.id;
16218
- const firstCalendar = calendars[0];
16219
- if (firstCalendar) return firstCalendar.id;
16220
- return null;
16224
+ function buildLoadTimeBaselines(raw) {
16225
+ const isMilestone = raw.type === "milestone";
16226
+ return {
16227
+ old_progress_registered: Number(raw.progress),
16228
+ old_duration_registered: isMilestone ? 0 : Number(raw.duration),
16229
+ old_cost_registered: Number(raw.cost),
16230
+ old_used_cost_registered: Number(raw.used_cost ?? 0),
16231
+ old_real_cost_registered: Number(raw.real_cost ?? 0),
16232
+ old_hhWorkTime_registered: Number(raw.hhWorkTime)
16233
+ };
16221
16234
  }
16222
16235
  function resolveCalendarId(rawCalendarId, knownCalendarIds, defaultCalendarId) {
16223
16236
  if (rawCalendarId == null) return defaultCalendarId;
16224
16237
  const coerced = String(rawCalendarId);
16225
16238
  return knownCalendarIds.has(coerced) ? coerced : defaultCalendarId;
16226
16239
  }
16227
- function parseLinksDroppingDanglingReferences(backendLinks, knownActivityIds, hoursPerDay, reporter) {
16228
- const parsedLinks = [];
16229
- for (const backendLink of backendLinks) {
16230
- try {
16231
- parsedLinks.push(parseLink(backendLink, knownActivityIds, hoursPerDay));
16232
- } catch (parseError2) {
16233
- reporter.warn("[parseFromBackend] dropping invalid link", parseError2);
16234
- }
16240
+ function parseNewActivitiesArray(raw) {
16241
+ if (raw == null || raw === "") return [];
16242
+ if (Array.isArray(raw)) return raw.map(String);
16243
+ if (typeof raw !== "string") return [];
16244
+ try {
16245
+ const parsed2 = JSON.parse(raw);
16246
+ return Array.isArray(parsed2) ? parsed2.map(String) : [];
16247
+ } catch {
16248
+ return [];
16235
16249
  }
16236
- return dedupeLinksBySignature(parsedLinks);
16237
16250
  }
16238
- function dedupeLinksBySignature(links) {
16239
- const bySignature = /* @__PURE__ */ new Map();
16240
- for (const link of links) {
16241
- const signature = `${link.source}-${link.target}-${link.type}`;
16242
- const existing = bySignature.get(signature);
16243
- if (!existing || linkIdAsNumber(link) > linkIdAsNumber(existing)) {
16244
- bySignature.set(signature, link);
16245
- }
16251
+ function hasUsableId(entry) {
16252
+ if (typeof entry !== "object" || entry === null) return false;
16253
+ if (!("id" in entry)) return false;
16254
+ const value = entry.id;
16255
+ return typeof value === "string" || typeof value === "number";
16256
+ }
16257
+ function parsePendingRequests(raw) {
16258
+ if (!Array.isArray(raw)) return [];
16259
+ const requests = [];
16260
+ for (const entry of raw) {
16261
+ if (hasUsableId(entry)) requests.push({ id: String(entry.id) });
16246
16262
  }
16247
- return [...bySignature.values()];
16263
+ return requests;
16248
16264
  }
16249
- function linkIdAsNumber(link) {
16250
- const n = Number(link.id);
16251
- return Number.isFinite(n) ? n : -Infinity;
16252
- }
16253
- function parseSector(s) {
16254
- const raw = s;
16255
- const customIdPrefix = typeof raw.custom_id_prefix === "string" && raw.custom_id_prefix.length > 0 ? raw.custom_id_prefix : void 0;
16256
- const incrementRaw = raw.custom_id_increment;
16257
- const customIdIncrement = typeof incrementRaw === "string" ? incrementRaw : typeof incrementRaw === "number" && Number.isFinite(incrementRaw) ? String(incrementRaw) : void 0;
16265
+ function parseActivity(raw, context) {
16266
+ const id = String(raw.unique_id);
16267
+ const parent = raw.parent_id === ROOT_PARENT_ID ? ROOT_PARENT_ID : String(raw.parent_id);
16268
+ const calendarId = resolveCalendarId(
16269
+ raw.calendarId,
16270
+ context.knownCalendarIds,
16271
+ context.defaultCalendarId
16272
+ );
16273
+ const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
16274
+ const assignedDefaultCalendar = calendarId != null && inheritedDefaultCalendar;
16275
+ const progress = Number(raw.progress);
16276
+ const { constraint_type: constraintType, constraint_date: constraintDate } = resolveConstraint(raw, context.reporter);
16277
+ const {
16278
+ start_date: startDate,
16279
+ end_date: endDate,
16280
+ duration
16281
+ } = resolveDatesAndDuration(raw);
16258
16282
  return {
16259
- id: String(s.id),
16260
- name: s.name,
16261
- hoursPerDay: s.hoursPerDay,
16262
- hoursPerWeek: s.hoursPerWeek,
16263
- dateFormat: s.dateFormat,
16264
- projectId: s.projectId,
16265
- companyId: s.companyId,
16266
- ...customIdPrefix !== void 0 ? { customIdPrefix } : {},
16267
- ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
16268
- ...s.update_duration_for_primavera_for_end_date === true ? { updateDurationForPrimaveraEndDate: true } : {},
16269
- activityCreter: parseActivityCreter(raw.activity_creter),
16270
- // Not a backend field: a complete sector needs it; the client value
16271
- // overrides this default at init via `resolveStatusCriteria`.
16272
- statusCriteria: DEFAULT_STATUS_CRITERIA
16283
+ ...proplannerIdField(raw),
16284
+ id,
16285
+ parent,
16286
+ text: raw.name,
16287
+ description: raw.description ?? "",
16288
+ type: raw.type,
16289
+ start_date: startDate,
16290
+ end_date: endDate,
16291
+ duration,
16292
+ constraint_type: constraintType,
16293
+ constraint_date: constraintDate,
16294
+ auto_scheduling: isAutoSchedulable(progress),
16295
+ calendar_id: calendarId,
16296
+ assignedDefaultCalendar,
16297
+ progress,
16298
+ cost: Number(raw.cost),
16299
+ used_cost: Number(raw.used_cost ?? 0),
16300
+ real_cost: Number(raw.real_cost ?? 0),
16301
+ hhWorkTime: Number(raw.hhWorkTime),
16302
+ real_work: Number(raw.real_work ?? 0),
16303
+ ponderator: Number(raw.ponderator),
16304
+ correlative_id: raw.correlative_id,
16305
+ is_lookahead: raw.isOnLookahead,
16306
+ visible: raw.should_be_showed !== false,
16307
+ newActivitiesArray: parseNewActivitiesArray(raw.newActivitiesArray),
16308
+ activityModifications: raw.activitymodifications,
16309
+ unique_correlative_id: raw.unique_correlative_id,
16310
+ ganttId: raw.ganttId,
16311
+ sectorId: raw.sectorId,
16312
+ companyId: raw.companyId,
16313
+ subcontractId: raw.subcontractId,
16314
+ subcontract: raw.subcontract,
16315
+ custom_id: raw.custom_id,
16316
+ hasCustomPonderator: raw.hasCustomPonderator,
16317
+ has_childs: raw.has_childs,
16318
+ sumOfDurationRecursively: raw.sumOfDurationRecursively,
16319
+ freeSlack: raw.freeSlack,
16320
+ is_critical: raw.is_critical,
16321
+ hasNewActivities: raw.hasNewActivities,
16322
+ isNewActivity: raw.isNewActivity,
16323
+ tasks: raw.tasks,
16324
+ responsables: raw.responsables,
16325
+ tags: raw.tags,
16326
+ createdAt: raw.createdAt,
16327
+ updatedAt: raw.updatedAt,
16328
+ pendingRequests: parsePendingRequests(raw.pendingRequests),
16329
+ for_disable_milestone_duration: Math.round(duration * context.hoursPerDay),
16330
+ ...buildLoadTimeBaselines(raw),
16331
+ ...raw.date_origin !== void 0 ? {
16332
+ date_origin: raw.date_origin === null ? null : parseBackendDate(raw.date_origin)
16333
+ } : {},
16334
+ ...raw.baseline_points !== void 0 ? { baseline_points: raw.baseline_points } : {}
16273
16335
  };
16274
16336
  }
16275
- function isAutoSchedulable(progress) {
16276
- return progress === 0;
16337
+ function forceRootProjectToRoot(activities) {
16338
+ const rootProject = activities.find(
16339
+ (activity) => activity.correlative_id === 0 && activity.type === "project"
16340
+ );
16341
+ if (rootProject) rootProject.parent = ROOT_PARENT_ID;
16277
16342
  }
16278
- function parseActivityCreter(raw) {
16279
- if (typeof raw !== "string") return DEFAULT_ACTIVITY_CRETER;
16280
- const upper = raw.toUpperCase();
16281
- return VALID_ACTIVITY_CRETER_VALUES.has(upper) ? upper : DEFAULT_ACTIVITY_CRETER;
16343
+ function reparentOrphansToRoot(activities, ids, reporter) {
16344
+ for (const activity of activities) {
16345
+ if (activity.parent === ROOT_PARENT_ID) continue;
16346
+ if (!ids.has(activity.parent)) {
16347
+ reporter.warn(
16348
+ `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parent}" \u2014 re-parenting to root ('0')`
16349
+ );
16350
+ activity.parent = ROOT_PARENT_ID;
16351
+ }
16352
+ }
16282
16353
  }
16283
- function parseCalendar(c) {
16284
- const workingMask = parseWorkingDays(c.working_days);
16285
- const { days, hours } = parseWorktimeDays(c, workingMask);
16286
- const dates = parseExceptionDays(c.exceptiondays);
16354
+
16355
+ // src/boundary/backend/normalize-calendar.ts
16356
+ function normalizeCalendars(schedulingCalendars, sectorCalendars, rawBaseCalendars) {
16357
+ const hasSchedulingCalendars = schedulingCalendars !== void 0 && schedulingCalendars.length > 0;
16358
+ const source = hasSchedulingCalendars ? schedulingCalendars : sectorCalendars ?? [];
16359
+ const calendars = source.map(parseSchedulingCalendar);
16360
+ const baseCalendars = (rawBaseCalendars ?? []).map(parseBaseCalendar);
16361
+ return {
16362
+ calendars,
16363
+ baseCalendars,
16364
+ knownCalendarIds: new Set(calendars.map((calendar) => calendar.id)),
16365
+ defaultCalendarId: pickDefaultCalendarId(calendars)
16366
+ };
16367
+ }
16368
+ function pickDefaultCalendarId(calendars) {
16369
+ const flaggedDefault = calendars.find((calendar) => calendar.is_default);
16370
+ if (flaggedDefault) return flaggedDefault.id;
16371
+ const firstCalendar = calendars[0];
16372
+ if (firstCalendar) return firstCalendar.id;
16373
+ return null;
16374
+ }
16375
+ function buildCalendar(raw, shape) {
16376
+ const workingMask = parseWorkingDays(raw.working_days);
16377
+ const { days, hours } = parseWorktimeDays(raw, workingMask);
16378
+ const dates = parseExceptionDays(shape.exceptionsSource);
16287
16379
  const worktime = { days, hours };
16288
16380
  if (dates) worktime.dates = dates;
16289
- const rawBaseDefault = c.baseDefault;
16290
16381
  return {
16291
- id: String(c.id),
16292
- name: c.name,
16293
- is_default: Boolean(c.is_default),
16294
- ...typeof rawBaseDefault === "boolean" ? { baseDefault: rawBaseDefault } : {},
16382
+ id: `${String(raw.id)}${shape.idSuffix}`,
16383
+ name: raw.name,
16384
+ is_default: shape.isDefault,
16385
+ ...shape.baseDefault !== void 0 ? { baseDefault: shape.baseDefault } : {},
16295
16386
  worktime
16296
16387
  };
16297
16388
  }
16389
+ function parseSchedulingCalendar(raw) {
16390
+ const rawBaseDefault = raw.baseDefault;
16391
+ const baseDefault = typeof rawBaseDefault === "boolean" ? rawBaseDefault : void 0;
16392
+ return buildCalendar(raw, {
16393
+ idSuffix: "",
16394
+ isDefault: Boolean(raw.is_default),
16395
+ baseDefault,
16396
+ exceptionsSource: raw.exceptiondays
16397
+ });
16398
+ }
16399
+ function parseBaseCalendar(raw) {
16400
+ return buildCalendar(raw, {
16401
+ idSuffix: "-base",
16402
+ isDefault: false,
16403
+ baseDefault: Boolean(raw.is_default),
16404
+ exceptionsSource: raw.baseexceptiondays ?? raw.exceptiondays
16405
+ });
16406
+ }
16298
16407
  function parseExceptionDays(exceptions) {
16299
16408
  if (!Array.isArray(exceptions) || exceptions.length === 0) return null;
16300
16409
  const map = {};
@@ -16330,9 +16439,8 @@ function parseExceptionDate(raw) {
16330
16439
  const year = Number(match[1]);
16331
16440
  const month = Number(match[2]);
16332
16441
  const day = Number(match[3]);
16333
- if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
16334
- return null;
16335
- }
16442
+ const isValidDate4 = Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day);
16443
+ if (!isValidDate4) return null;
16336
16444
  return Date.UTC(year, month - 1, day);
16337
16445
  }
16338
16446
  function exceptionToDateValue(exception) {
@@ -16344,17 +16452,18 @@ function exceptionToDateValue(exception) {
16344
16452
  }
16345
16453
  return void 0;
16346
16454
  }
16455
+ var TIME_OF_DAY = /^\d{1,2}:\d{2}$/;
16347
16456
  function extractExceptionWindows(shifts) {
16348
16457
  const windows = [];
16349
16458
  for (const shift of shifts) {
16350
16459
  const csvs = splitShiftString(shift?.shift_string);
16351
16460
  if (!csvs) continue;
16352
- if (!csvs.startsCsv.includes(",") && !csvs.endsCsv.includes(",")) {
16461
+ const isCompactWindow = !csvs.startsCsv.includes(",") && !csvs.endsCsv.includes(",");
16462
+ if (isCompactWindow) {
16353
16463
  const start = csvs.startsCsv.trim();
16354
16464
  const end = csvs.endsCsv.trim();
16355
- if (!isMissingValue(start) && !isMissingValue(end) && TIME_OF_DAY.test(start) && TIME_OF_DAY.test(end)) {
16356
- windows.push(`${start}-${end}`);
16357
- }
16465
+ const isUsableWindow = !isMissingValue(start) && !isMissingValue(end) && TIME_OF_DAY.test(start) && TIME_OF_DAY.test(end);
16466
+ if (isUsableWindow) windows.push(`${start}-${end}`);
16358
16467
  continue;
16359
16468
  }
16360
16469
  const perDay = shiftCsvToPerDayWindows(csvs.startsCsv, csvs.endsCsv);
@@ -16364,17 +16473,16 @@ function extractExceptionWindows(shifts) {
16364
16473
  windows.sort(compareWindowByStart);
16365
16474
  return windows;
16366
16475
  }
16367
- var TIME_OF_DAY = /^\d{1,2}:\d{2}$/;
16368
16476
  function windowStartMinutes(window2) {
16369
16477
  const dash = window2.indexOf("-");
16370
16478
  const start = dash > 0 ? window2.slice(0, dash) : window2;
16371
- const [h, m] = start.split(":");
16372
- return (Number(h) || 0) * 60 + (Number(m) || 0);
16479
+ const [hour, minute] = start.split(":");
16480
+ return (Number(hour) || 0) * 60 + (Number(minute) || 0);
16373
16481
  }
16374
- function compareWindowByStart(a, b) {
16375
- return windowStartMinutes(a) - windowStartMinutes(b);
16482
+ function compareWindowByStart(first, second) {
16483
+ return windowStartMinutes(first) - windowStartMinutes(second);
16376
16484
  }
16377
- function parseWorktimeDays(c, workingMask) {
16485
+ function parseWorktimeDays(raw, workingMask) {
16378
16486
  const windowsByDay = Array.from({ length: 7 }, () => []);
16379
16487
  const addCsvPair = (startsCsv, endsCsv) => {
16380
16488
  const perDay = shiftCsvToPerDayWindows(startsCsv, endsCsv);
@@ -16384,13 +16492,15 @@ function parseWorktimeDays(c, workingMask) {
16384
16492
  if (window2 && dayWindows) dayWindows.push(window2);
16385
16493
  }
16386
16494
  };
16387
- if (Array.isArray(c.shifts) && c.shifts.length > 0) {
16388
- for (const shift of c.shifts) {
16495
+ const hasNestedShifts = Array.isArray(raw.shifts) && raw.shifts.length > 0;
16496
+ const hasFlatShift = typeof raw.shift_start === "string" && typeof raw.shift_end === "string";
16497
+ if (hasNestedShifts) {
16498
+ for (const shift of raw.shifts) {
16389
16499
  const csvs = splitShiftString(shift?.shift_string);
16390
16500
  if (csvs) addCsvPair(csvs.startsCsv, csvs.endsCsv);
16391
16501
  }
16392
- } else if (typeof c.shift_start === "string" && typeof c.shift_end === "string") {
16393
- addCsvPair(c.shift_start, c.shift_end);
16502
+ } else if (hasFlatShift) {
16503
+ addCsvPair(raw.shift_start, raw.shift_end);
16394
16504
  }
16395
16505
  for (const dayWindows of windowsByDay) dayWindows.sort(compareWindowByStart);
16396
16506
  const firstWorkingWindows = windowsByDay.find(
@@ -16412,11 +16522,10 @@ function shiftCsvToPerDayWindows(startsCsv, endsCsv) {
16412
16522
  if (typeof startsCsv !== "string" || typeof endsCsv !== "string") {
16413
16523
  return empty;
16414
16524
  }
16415
- const starts = startsCsv.split(",").map((s) => s.trim());
16416
- const ends = endsCsv.split(",").map((s) => s.trim());
16417
- if (starts.length !== ends.length || starts.length === 0 || starts.length > WEEKDAYS_PER_WEEK) {
16418
- return empty;
16419
- }
16525
+ const starts = startsCsv.split(",").map((value) => value.trim());
16526
+ const ends = endsCsv.split(",").map((value) => value.trim());
16527
+ const isAlignedWeek = starts.length === ends.length && starts.length > 0 && starts.length <= WEEKDAYS_PER_WEEK;
16528
+ if (!isAlignedWeek) return empty;
16420
16529
  const windows = [...empty];
16421
16530
  for (let i = 0; i < starts.length; i += 1) {
16422
16531
  const start = starts[i];
@@ -16437,196 +16546,48 @@ function splitShiftString(raw) {
16437
16546
  }
16438
16547
  function parseWorkingDays(csv) {
16439
16548
  if (typeof csv !== "string") return [...CALENDAR_FALLBACK_WORKING_DAYS];
16440
- const parts = csv.split(",").map((s) => s.trim());
16441
- if (parts.length !== WEEKDAYS_PER_WEEK)
16549
+ const parts = csv.split(",").map((value) => value.trim());
16550
+ if (parts.length !== WEEKDAYS_PER_WEEK) {
16442
16551
  return [...CALENDAR_FALLBACK_WORKING_DAYS];
16443
- return parts.map((p) => WORKING_DAY_TRUE_VALUES.has(p));
16552
+ }
16553
+ return parts.map((part) => WORKING_DAY_TRUE_VALUES.has(part));
16444
16554
  }
16445
16555
  var MISSING_SHIFT_VALUES = /* @__PURE__ */ new Set(["false", "null"]);
16446
16556
  function isMissingValue(value) {
16447
16557
  return !value || MISSING_SHIFT_VALUES.has(value);
16448
16558
  }
16449
- var ACTIVITY_PASSTHROUGH_FIELDS = /* @__PURE__ */ new Set([
16450
- // Ids and relations.
16451
- "unique_correlative_id",
16452
- "ganttId",
16453
- "sectorId",
16454
- "companyId",
16455
- "subcontractId",
16456
- "subcontract",
16457
- "custom_id",
16458
- // Computed flags / metrics from the backend.
16459
- "hasCustomPonderator",
16460
- "has_childs",
16461
- "sumOfDurationRecursively",
16462
- "freeSlack",
16463
- "is_critical",
16464
- "hasNewActivities",
16465
- "isNewActivity",
16466
- // Live validation field read by endDatePipeline; explicit so it survives by design.
16467
- "date_origin",
16468
- "baseline_points",
16469
- // Relation arrays.
16470
- "tasks",
16471
- "responsables",
16472
- "tags",
16473
- // `pendingRequests` is NOT passthrough — parsed explicitly below to the
16474
- // typed `{ id: string }[]` shape (SIR freeze authority, A1).
16475
- // Timestamps.
16476
- "createdAt",
16477
- "updatedAt"
16478
- ]);
16479
- function proplannerIdField(a) {
16480
- return typeof a.id === "number" ? { proplannerId: a.id } : {};
16481
- }
16482
- function collectPassthrough(a) {
16483
- const passthrough = {};
16484
- for (const key of ACTIVITY_PASSTHROUGH_FIELDS) {
16485
- if (key in a) {
16486
- passthrough[key] = a[key];
16487
- }
16488
- }
16489
- return passthrough;
16490
- }
16491
- function resolveConstraint(a, reporter) {
16492
- const constraintType = parseConstraintLabel(a.constraint, reporter);
16493
- const constraintTakesDate = constraintType !== "asap" && constraintType !== "alap";
16494
- const constraintDate = constraintTakesDate && a.constraint_date ? parseBackendDate(a.constraint_date) : null;
16495
- return { constraint_type: constraintType, constraint_date: constraintDate };
16496
- }
16497
- function resolveDatesAndDuration(a) {
16498
- const startDate = parseBackendDate(a.start_date);
16499
- const isMilestone = a.type === "milestone";
16500
- return {
16501
- start_date: startDate,
16502
- end_date: isMilestone ? startDate : parseBackendDate(a.end_date),
16503
- duration: isMilestone ? 0 : Number(a.duration)
16504
- };
16505
- }
16506
- function buildLoadTimeBaselines(a) {
16507
- const isMilestone = a.type === "milestone";
16508
- return {
16509
- old_progress_registered: Number(a.progress),
16510
- old_duration_registered: isMilestone ? 0 : Number(a.duration),
16511
- old_cost_registered: Number(a.cost),
16512
- old_used_cost_registered: Number(a.used_cost ?? 0),
16513
- old_real_cost_registered: Number(a.real_cost ?? 0),
16514
- old_hhWorkTime_registered: Number(a.hhWorkTime)
16515
- };
16516
- }
16517
- function parseActivity(a, calendarIds, defaultCalendarId, reporter) {
16518
- const id = String(a.unique_id);
16519
- const parent = a.parent_id === ROOT_PARENT_ID ? ROOT_PARENT_ID : String(a.parent_id);
16520
- const calendarId = resolveCalendarId(
16521
- a.calendarId,
16522
- calendarIds,
16523
- defaultCalendarId
16524
- );
16525
- const assignedDefaultCalendar = calendarId != null && (a.calendarId == null || !calendarIds.has(String(a.calendarId)));
16526
- const progress = Number(a.progress);
16527
- const autoScheduling = isAutoSchedulable(progress);
16528
- const { constraint_type: constraintType, constraint_date: constraintDate } = resolveConstraint(a, reporter);
16529
- const {
16530
- start_date: startDate,
16531
- end_date: endDate,
16532
- duration
16533
- } = resolveDatesAndDuration(a);
16534
- return {
16535
- ...collectPassthrough(a),
16536
- ...proplannerIdField(a),
16537
- id,
16538
- parent,
16539
- text: a.name,
16540
- description: a.description ?? "",
16541
- type: a.type,
16542
- start_date: startDate,
16543
- end_date: endDate,
16544
- duration,
16545
- constraint_type: constraintType,
16546
- constraint_date: constraintDate,
16547
- progress,
16548
- cost: Number(a.cost),
16549
- used_cost: Number(a.used_cost ?? 0),
16550
- real_cost: Number(a.real_cost ?? 0),
16551
- hhWorkTime: Number(a.hhWorkTime),
16552
- real_work: Number(a.real_work ?? 0),
16553
- ponderator: Number(a.ponderator),
16554
- calendar_id: calendarId,
16555
- assignedDefaultCalendar,
16556
- auto_scheduling: autoScheduling,
16557
- correlative_id: a.correlative_id,
16558
- is_lookahead: a.isOnLookahead,
16559
- // View-model visibility bit: only an explicit backend `false` hides.
16560
- visible: a.should_be_showed !== false,
16561
- ...buildLoadTimeBaselines(a),
16562
- pendingRequests: parsePendingRequests(a.pendingRequests),
16563
- activityModifications: a.activitymodifications,
16564
- // D5 fix (2026-05-19): backend serializes newActivitiesArray as a
16565
- // JSON-encoded string; the in-memory form must be a real array so dispatch
16566
- // pipelines can iterate/mutate it. See DIVERGENCES_activity_create.md D5.
16567
- newActivitiesArray: parseNewActivitiesArray(a.newActivitiesArray)
16568
- // Cast: TS can't statically verify the `...collectPassthrough` spread
16569
- // provides every required Activity field (the loop iterates a runtime Set,
16570
- // not a typed contract). When the BE responds normally every passthrough
16571
- // field is present; a missing field is a real data bug to surface at the
16572
- // source, not mask via optional typing. `ActivityRuntime`
16573
- // (= Activity & DhtmlxHeritageFields) because the literal includes the
16574
- // old_*_registered baselines (DHTMLX heritage — G1); it is assignable to
16575
- // Activity.
16576
- };
16577
- }
16578
- function parseNewActivitiesArray(raw) {
16579
- if (raw == null || raw === "") return [];
16580
- if (Array.isArray(raw)) return raw.map(String);
16581
- if (typeof raw !== "string") return [];
16582
- try {
16583
- const parsed2 = JSON.parse(raw);
16584
- return Array.isArray(parsed2) ? parsed2.map(String) : [];
16585
- } catch {
16586
- return [];
16587
- }
16588
- }
16589
- function parsePendingRequests(raw) {
16590
- if (!Array.isArray(raw)) return [];
16591
- return raw.filter((r) => r != null && typeof r === "object").map((r) => ({ id: String(r.id) }));
16559
+
16560
+ // src/boundary/backend/normalize-link.ts
16561
+ function isLinkTypeCode(value) {
16562
+ return value === "0" || value === "1" || value === "2" || value === "3";
16592
16563
  }
16593
- var CONSTRAINT_LABEL_MAP = {
16594
- "AS SOON AS POSSIBLE": "asap",
16595
- "AS LATE AS POSSIBLE": "alap",
16596
- "START NO EARLIER THAN": "snet",
16597
- "START NO LATER THAN": "snlt",
16598
- "FINISH NO EARLIER THAN": "fnet",
16599
- "FINISH NO LATER THAN": "fnlt",
16600
- "MUST START ON": "mso",
16601
- "MUST FINISH ON": "mfo"
16602
- };
16603
- function parseConstraintLabel(label, reporter) {
16604
- if (!label) return null;
16605
- const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
16606
- if (normalized === void 0) {
16607
- reporter.warn(
16608
- `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
16609
- );
16610
- return DEFAULT_CONSTRAINT_TYPE;
16611
- }
16612
- return normalized;
16564
+ function resolveLinkType(rawType) {
16565
+ if (typeof rawType !== "string") return null;
16566
+ const fromLabel = parseLinkTypeCode(rawType);
16567
+ if (fromLabel) return fromLabel;
16568
+ if (isLinkTypeCode(rawType)) return rawType;
16569
+ return null;
16613
16570
  }
16614
- function reparentOrphansToRoot(activities, ids, reporter) {
16615
- for (const activity of activities) {
16616
- if (activity.parent === ROOT_PARENT_ID) continue;
16617
- if (!ids.has(activity.parent)) {
16618
- reporter.warn(
16619
- `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parent}" \u2014 re-parenting to root ('0')`
16571
+ function normalizeLinks(rawLinks, context) {
16572
+ const parsedLinks = [];
16573
+ for (const rawLink of rawLinks) {
16574
+ try {
16575
+ parsedLinks.push(
16576
+ parseLink(rawLink, context.knownActivityIds, context.hoursPerDay)
16577
+ );
16578
+ } catch (parseError2) {
16579
+ context.reporter.warn(
16580
+ "[parseFromBackend] dropping invalid link",
16581
+ parseError2
16620
16582
  );
16621
- activity.parent = ROOT_PARENT_ID;
16622
16583
  }
16623
16584
  }
16585
+ return dedupeLinksBySignature(parsedLinks);
16624
16586
  }
16625
- function parseLink(l, activityIds, hoursPerDay) {
16626
- const lr = l;
16627
- const id = String(lr.unique_id ?? lr.id);
16628
- const source = String(lr.source_id ?? lr.source ?? "");
16629
- const target = String(lr.target_id ?? lr.target ?? "");
16587
+ function parseLink(link, activityIds, hoursPerDay) {
16588
+ const id = String(link.unique_id ?? link.id);
16589
+ const source = String(link.source_id ?? link.source ?? "");
16590
+ const target = String(link.target_id ?? link.target ?? "");
16630
16591
  if (!activityIds.has(source)) {
16631
16592
  throw new Error(
16632
16593
  `[parseFromBackend] link "${id}" source "${source}" does not exist`
@@ -16637,24 +16598,108 @@ function parseLink(l, activityIds, hoursPerDay) {
16637
16598
  `[parseFromBackend] link "${id}" target "${target}" does not exist`
16638
16599
  );
16639
16600
  }
16640
- const rawType = String(lr.type ?? "fs");
16641
- const type = parseLinkTypeCode(rawType) ?? rawType;
16642
- const ganttIdRaw = lr.ganttId;
16643
- const sectorIdRaw = lr.sectorId;
16644
- const proplannerIdRaw = lr.id;
16601
+ const type = resolveLinkType(link.type);
16602
+ if (type === null) {
16603
+ throw new Error(
16604
+ `[parseFromBackend] link "${id}" has invalid type "${String(link.type)}"`
16605
+ );
16606
+ }
16607
+ const ganttIdRaw = link.ganttId;
16608
+ const sectorIdRaw = link.sectorId;
16609
+ const proplannerIdRaw = link.id;
16645
16610
  return {
16646
16611
  id,
16647
16612
  source,
16648
16613
  target,
16649
16614
  type,
16650
- // Backend ships `lag` in DAYS; the core stores it internally in WORKING
16651
- // HOURS (mirrors `duration`). Convert here — the single backend boundary.
16652
- lag: lagDaysToHours(Number(lr.lag ?? 0), hoursPerDay),
16615
+ lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
16653
16616
  ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
16654
16617
  ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
16655
16618
  ...typeof proplannerIdRaw === "number" ? { proplannerId: proplannerIdRaw } : {}
16656
16619
  };
16657
16620
  }
16621
+ function dedupeLinksBySignature(links) {
16622
+ const bySignature = /* @__PURE__ */ new Map();
16623
+ for (const link of links) {
16624
+ const signature = `${link.source}-${link.target}-${link.type}`;
16625
+ const existing = bySignature.get(signature);
16626
+ if (!existing || linkIdAsNumber(link) > linkIdAsNumber(existing)) {
16627
+ bySignature.set(signature, link);
16628
+ }
16629
+ }
16630
+ return [...bySignature.values()];
16631
+ }
16632
+ function linkIdAsNumber(link) {
16633
+ const numeric = Number(link.id);
16634
+ return Number.isFinite(numeric) ? numeric : -Infinity;
16635
+ }
16636
+
16637
+ // src/boundary/backend/normalize-sector.ts
16638
+ function parseActivityCreter(raw) {
16639
+ if (typeof raw !== "string") return DEFAULT_ACTIVITY_CRETER;
16640
+ switch (raw.toUpperCase()) {
16641
+ case "DURATION":
16642
+ return "DURATION";
16643
+ case "COST":
16644
+ return "COST";
16645
+ case "HH":
16646
+ return "HH";
16647
+ default:
16648
+ return DEFAULT_ACTIVITY_CRETER;
16649
+ }
16650
+ }
16651
+ function readCustomIdPrefix(sector) {
16652
+ const prefix = sector.custom_id_prefix;
16653
+ const hasPrefix = typeof prefix === "string" && prefix.length > 0;
16654
+ return hasPrefix ? prefix : void 0;
16655
+ }
16656
+ function readCustomIdIncrement(sector) {
16657
+ const increment = sector.custom_id_increment;
16658
+ if (typeof increment === "string") return increment;
16659
+ const isFiniteNumber = typeof increment === "number" && Number.isFinite(increment);
16660
+ return isFiniteNumber ? String(increment) : void 0;
16661
+ }
16662
+ function parseSector(sector) {
16663
+ const customIdPrefix = readCustomIdPrefix(sector);
16664
+ const customIdIncrement = readCustomIdIncrement(sector);
16665
+ const isPrimaveraEndDate = sector.update_duration_for_primavera_for_end_date === true;
16666
+ return {
16667
+ id: String(sector.id),
16668
+ name: sector.name,
16669
+ hoursPerDay: sector.hoursPerDay,
16670
+ hoursPerWeek: sector.hoursPerWeek,
16671
+ dateFormat: sector.dateFormat,
16672
+ projectId: sector.projectId,
16673
+ companyId: sector.companyId,
16674
+ ...customIdPrefix !== void 0 ? { customIdPrefix } : {},
16675
+ ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
16676
+ ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
16677
+ activityCreter: parseActivityCreter(sector.activity_creter),
16678
+ statusCriteria: DEFAULT_STATUS_CRITERIA
16679
+ };
16680
+ }
16681
+
16682
+ // src/boundary/backend/parse-from-backend.ts
16683
+ function parseFromBackend(input, reporter = noopReporter) {
16684
+ const sector = parseSector(input.sector);
16685
+ const { calendars, baseCalendars, knownCalendarIds, defaultCalendarId } = normalizeCalendars(
16686
+ input.calendars,
16687
+ input.sector.calendars,
16688
+ input.baseCalendars
16689
+ );
16690
+ const { activities, activityIds } = normalizeActivities(input.activities, {
16691
+ knownCalendarIds,
16692
+ defaultCalendarId,
16693
+ hoursPerDay: sector.hoursPerDay,
16694
+ reporter
16695
+ });
16696
+ const links = normalizeLinks(input.links, {
16697
+ knownActivityIds: activityIds,
16698
+ hoursPerDay: sector.hoursPerDay,
16699
+ reporter
16700
+ });
16701
+ return { sector, activities, links, calendars, baseCalendars };
16702
+ }
16658
16703
 
16659
16704
  // src/generators/activity-id-generator.ts
16660
16705
  function createActivityIdGenerator(options = {}) {