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