@7365admin1/core 3.21.1 → 3.22.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8590d35: release changes in vehicle api
8
+
3
9
  ## 3.21.1
4
10
 
5
11
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -2014,6 +2014,14 @@ declare function useBuildingService(): {
2014
2014
  uploadSpreadsheetBuilding: (data: any[], site: string) => Promise<{
2015
2015
  buildingPayloads: any[];
2016
2016
  buildingUnitPayloads: any[];
2017
+ insertedBuildings: number;
2018
+ insertedUnits: number;
2019
+ skippedRows: {
2020
+ row: number;
2021
+ block: number;
2022
+ name: string;
2023
+ reason: string;
2024
+ }[];
2017
2025
  }>;
2018
2026
  };
2019
2027
 
package/dist/index.js CHANGED
@@ -24143,6 +24143,7 @@ function useDahuaService() {
24143
24143
  return match ? match[1] : null;
24144
24144
  }
24145
24145
  async function addPlateNumber(value) {
24146
+ console.log("addPlateNumber called with value:", value);
24146
24147
  let recno = null;
24147
24148
  const validation = import_joi40.default.object({
24148
24149
  host: import_joi40.default.string().required(),
@@ -24160,7 +24161,7 @@ function useDahuaService() {
24160
24161
  throw new import_node_server_utils72.BadRequestError(`Validation error: ${validationError.message}`);
24161
24162
  }
24162
24163
  try {
24163
- value.owner = String(value.owner ?? "").substring(0, 15) || "unknown";
24164
+ value.owner = String(value.owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
24164
24165
  const formatDahuaDate2 = (dateStr, fallbackYearsAhead = 0) => {
24165
24166
  const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
24166
24167
  if (!dateStr) {
@@ -27027,58 +27028,25 @@ function useVehicleService() {
27027
27028
  for (const camera of siteCameras) {
27028
27029
  const { host, username, password } = camera;
27029
27030
  const plateNumber2 = vehicleValue.plateNumber;
27030
- const dahuaQuery = {
27031
+ const dahuaPayload = {
27031
27032
  host,
27032
27033
  username,
27033
27034
  password,
27034
27035
  plateNumber: plateNumber2,
27035
- mode: _mode
27036
+ mode: _mode,
27037
+ owner,
27038
+ ...startDateDahua ? { start: startDateDahua } : {},
27039
+ ...endDateDahua ? { end: endDateDahua } : {}
27036
27040
  };
27037
- const raw = await _getPlateNumber(dahuaQuery);
27038
- const parsed = parseDahuaFind(raw);
27039
- if (!parsed.exists) {
27040
- const dahuaPayload = {
27041
- host,
27042
- username,
27043
- password,
27044
- plateNumber: plateNumber2,
27045
- mode: _mode,
27046
- owner,
27047
- ...startDateDahua ? { start: startDateDahua } : {},
27048
- ...endDateDahua ? { end: endDateDahua } : {}
27049
- };
27050
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
27051
- const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
27052
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
27053
- if (responseStatus !== 200) {
27054
- throw new import_node_server_utils78.BadRequestError(
27055
- `Failed to add plate number to ANPR ${_type}: status=${responseStatus}, response=${responseData}`
27056
- );
27057
- }
27058
- vehicleValue.recNo = responseData.split("=")[1]?.trim();
27059
- } else {
27060
- const dahuaPayload = {
27061
- host,
27062
- username,
27063
- password,
27064
- plateNumber: plateNumber2,
27065
- recno: parsed.recNo,
27066
- mode: _mode,
27067
- ...startDateDahua ? { start: startDateDahua } : {},
27068
- ...endDateDahua ? { end: endDateDahua } : {},
27069
- owner,
27070
- isOpenGate: true
27071
- };
27072
- const dahuaResponse = await _updatePlateNumber(dahuaPayload);
27073
- const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
27074
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
27075
- if (responseStatus !== 200) {
27076
- throw new import_node_server_utils78.BadRequestError(
27077
- `Failed to update plate number to ANPR ${_type}: status=${responseStatus}, response=${responseData}`
27078
- );
27079
- }
27080
- vehicleValue.recNo = parsed.recNo;
27041
+ const dahuaResponse = await _addPlateNumber(dahuaPayload);
27042
+ const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
27043
+ const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
27044
+ if (responseStatus !== 200) {
27045
+ throw new import_node_server_utils78.BadRequestError(
27046
+ `Failed to add plate number to ANPR ${_type}: status=${responseStatus}, response=${responseData}`
27047
+ );
27081
27048
  }
27049
+ vehicleValue.recNo = responseData.split("=")[1]?.trim();
27082
27050
  if (value.peopleId && vehicleValue.recNo) {
27083
27051
  await _pushVehicleById(
27084
27052
  value.peopleId,
@@ -28609,10 +28577,22 @@ function useBuildingRepo() {
28609
28577
  {
28610
28578
  $lookup: {
28611
28579
  from: "building-levels",
28612
- localField: "levels",
28613
- foreignField: "_id",
28580
+ let: {
28581
+ buildingId: "$_id",
28582
+ buildingLevelIds: { $ifNull: ["$levels", []] }
28583
+ },
28614
28584
  pipeline: [
28615
- { $match: { status: "active" /* ACTIVE */ } },
28585
+ {
28586
+ $match: {
28587
+ status: "active" /* ACTIVE */,
28588
+ $expr: {
28589
+ $or: [
28590
+ { $in: ["$_id", "$$buildingLevelIds"] },
28591
+ { $eq: ["$blockId", "$$buildingId"] }
28592
+ ]
28593
+ }
28594
+ }
28595
+ },
28616
28596
  { $project: { _id: 1, name: 1 } }
28617
28597
  ],
28618
28598
  as: "levels"
@@ -29179,6 +29159,12 @@ function useBuildingLevelRepo() {
29179
29159
  message: `Failed to clear cache namespace for ${building_level_namespace_collection}: ${err.message}`
29180
29160
  });
29181
29161
  });
29162
+ delBuildingNamespace().catch((err) => {
29163
+ import_node_server_utils87.logger.log({
29164
+ level: "error",
29165
+ message: `Failed to clear buildings cache: ${err.message}`
29166
+ });
29167
+ });
29182
29168
  }
29183
29169
  async function add(value, session) {
29184
29170
  try {
@@ -29378,12 +29364,6 @@ function useBuildingLevelRepo() {
29378
29364
  { session }
29379
29365
  );
29380
29366
  delCachedData();
29381
- delBuildingNamespace().catch((err) => {
29382
- import_node_server_utils87.logger.log({
29383
- level: "error",
29384
- message: `Failed to clear buildings cache: ${err.message}`
29385
- });
29386
- });
29387
29367
  return "Successfully deleted building level";
29388
29368
  } catch (error) {
29389
29369
  import_node_server_utils87.logger.log({
@@ -29546,6 +29526,18 @@ function useBuildingService() {
29546
29526
  const { updateStatusById } = useFileRepo();
29547
29527
  const { deleteFile } = useFileService();
29548
29528
  const { bulkWriteLevels: _bulkWriteLevels } = useBuildingLevelRepo();
29529
+ function normalizeImportKey(value) {
29530
+ return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
29531
+ }
29532
+ function toObjectIdOrNull(value) {
29533
+ if (value instanceof import_mongodb54.ObjectId) {
29534
+ return value;
29535
+ }
29536
+ if (typeof value === "string" && import_mongodb54.ObjectId.isValid(value)) {
29537
+ return new import_mongodb54.ObjectId(value);
29538
+ }
29539
+ return null;
29540
+ }
29549
29541
  async function add(value) {
29550
29542
  const session = import_node_server_utils88.useAtlas.getClient()?.startSession();
29551
29543
  if (!session) {
@@ -29688,47 +29680,161 @@ function useBuildingService() {
29688
29680
  }
29689
29681
  async function uploadSpreadsheetBuilding(data, site) {
29690
29682
  const session = import_node_server_utils88.useAtlas.getClient()?.startSession();
29683
+ const db = import_node_server_utils88.useAtlas.getDb();
29691
29684
  if (!session) {
29692
29685
  throw new import_node_server_utils88.BadRequestError("Database session not available.");
29693
29686
  }
29687
+ if (!db) {
29688
+ throw new import_node_server_utils88.BadRequestError("Database not available.");
29689
+ }
29694
29690
  try {
29695
29691
  session.startTransaction();
29696
29692
  const blockMap = /* @__PURE__ */ new Map();
29693
+ const skippedRows = [];
29697
29694
  for (const row of data) {
29698
29695
  const block = row.block;
29699
29696
  const name = row.name;
29697
+ const rowNumber = row.__rowNumber;
29700
29698
  const rawLevel = row.level?.toString().trim();
29701
29699
  const rawUnit = row.unit?.toString().trim();
29702
29700
  const normalizedLevel = /^(na|n\/a)$/i.test(rawLevel) ? null : rawLevel;
29703
29701
  const normalizedUnit = /^(na|n\/a)$/i.test(rawUnit) ? null : rawUnit;
29704
29702
  if (!blockMap.has(block)) {
29705
- blockMap.set(block, { name, levels: [], unitsByLevel: /* @__PURE__ */ new Map() });
29703
+ blockMap.set(block, {
29704
+ name,
29705
+ levels: [],
29706
+ rows: [],
29707
+ unitsByLevel: /* @__PURE__ */ new Map(),
29708
+ unitRowsByLevel: /* @__PURE__ */ new Map()
29709
+ });
29706
29710
  }
29707
29711
  const blockEntry = blockMap.get(block);
29712
+ blockEntry.rows.push(rowNumber);
29708
29713
  if (blockEntry.name !== name) {
29709
29714
  blockEntry.name = `${blockEntry.name}, ${name}`;
29710
29715
  }
29711
- if (normalizedLevel && !blockEntry.levels.includes(normalizedLevel)) {
29716
+ if (normalizedLevel && !blockEntry.levels.some(
29717
+ (level) => normalizeImportKey(level) === normalizeImportKey(normalizedLevel)
29718
+ )) {
29712
29719
  blockEntry.levels.push(normalizedLevel);
29713
29720
  }
29714
29721
  if (normalizedLevel && normalizedUnit) {
29715
29722
  if (!blockEntry.unitsByLevel.has(normalizedLevel)) {
29716
29723
  blockEntry.unitsByLevel.set(normalizedLevel, []);
29717
29724
  }
29725
+ if (!blockEntry.unitRowsByLevel.has(normalizedLevel)) {
29726
+ blockEntry.unitRowsByLevel.set(normalizedLevel, /* @__PURE__ */ new Map());
29727
+ }
29718
29728
  const units = blockEntry.unitsByLevel.get(normalizedLevel);
29719
- if (!units.includes(normalizedUnit)) {
29729
+ const unitRows = blockEntry.unitRowsByLevel.get(normalizedLevel);
29730
+ if (!units.some(
29731
+ (unit) => normalizeImportKey(unit) === normalizeImportKey(normalizedUnit)
29732
+ )) {
29720
29733
  units.push(normalizedUnit);
29721
29734
  }
29735
+ unitRows.set(normalizedUnit, [
29736
+ ...unitRows.get(normalizedUnit) ?? [],
29737
+ rowNumber
29738
+ ]);
29722
29739
  }
29723
29740
  }
29724
29741
  const buildingPayloads = [];
29725
29742
  const buildingUnitPayloads = [];
29743
+ if (blockMap.size === 0) {
29744
+ await session.commitTransaction();
29745
+ return {
29746
+ buildingPayloads,
29747
+ buildingUnitPayloads,
29748
+ insertedBuildings: 0,
29749
+ insertedUnits: 0,
29750
+ skippedRows
29751
+ };
29752
+ }
29753
+ const buildingsCollection = db.collection(buildings_namespace_collection);
29754
+ const buildingLevelsCollection = db.collection(
29755
+ building_level_namespace_collection
29756
+ );
29757
+ const buildingUnitsCollection = db.collection(
29758
+ building_units_namespace_collection
29759
+ );
29760
+ const existingBuildings = await buildingsCollection.find(
29761
+ {
29762
+ site: new import_mongodb54.ObjectId(site),
29763
+ status: "active" /* ACTIVE */,
29764
+ $or: Array.from(blockMap.entries()).flatMap(([block, entry]) => [
29765
+ { block },
29766
+ { name: entry.name }
29767
+ ])
29768
+ },
29769
+ { session, projection: { block: 1, name: 1, levels: 1 } }
29770
+ ).toArray();
29771
+ const existingBuildingsByBlock = new Map(
29772
+ existingBuildings.map((building) => [building.block, building])
29773
+ );
29774
+ const existingBuildingsByName = new Map(
29775
+ existingBuildings.map((building) => [building.name, building])
29776
+ );
29777
+ const uploadedNames = /* @__PURE__ */ new Set();
29726
29778
  for (const [
29727
29779
  block,
29728
- { name, levels, unitsByLevel }
29780
+ { name, levels, rows, unitsByLevel, unitRowsByLevel }
29729
29781
  ] of blockMap.entries()) {
29730
- const buildingId = new import_mongodb54.ObjectId();
29731
- const levelsPayload = (levels ?? []).map((levelName) => ({
29782
+ const conflictReasons = [];
29783
+ const existingBuildingByBlock = existingBuildingsByBlock.get(block);
29784
+ const existingBuildingByName = existingBuildingsByName.get(name);
29785
+ if (existingBuildingByBlock && existingBuildingByBlock.name !== name) {
29786
+ conflictReasons.push(
29787
+ `Block ${block} already exists with building name "${existingBuildingByBlock.name}".`
29788
+ );
29789
+ }
29790
+ if (existingBuildingByName && existingBuildingByName.block !== block) {
29791
+ conflictReasons.push(
29792
+ `Building name "${name}" already exists in block ${existingBuildingByName.block}.`
29793
+ );
29794
+ }
29795
+ if (uploadedNames.has(name)) {
29796
+ conflictReasons.push(
29797
+ `Building name "${name}" is duplicated in this spreadsheet.`
29798
+ );
29799
+ }
29800
+ if (conflictReasons.length) {
29801
+ rows.forEach((row) => {
29802
+ skippedRows.push({
29803
+ row,
29804
+ block,
29805
+ name,
29806
+ reason: conflictReasons.join(" ")
29807
+ });
29808
+ });
29809
+ continue;
29810
+ }
29811
+ uploadedNames.add(name);
29812
+ const existingBuilding = existingBuildingByBlock ?? null;
29813
+ const buildingId = existingBuilding?._id ?? new import_mongodb54.ObjectId();
29814
+ const existingLevelIds = (existingBuilding?.levels ?? []).map(toObjectIdOrNull).filter(
29815
+ (levelId) => Boolean(levelId)
29816
+ );
29817
+ const existingLevels = existingBuilding || existingLevelIds.length > 0 ? await buildingLevelsCollection.find(
29818
+ {
29819
+ $or: [
29820
+ { blockId: buildingId },
29821
+ { blockId: buildingId.toString() },
29822
+ ...existingLevelIds.length > 0 ? [{ _id: { $in: existingLevelIds } }] : []
29823
+ ],
29824
+ status: "active" /* ACTIVE */
29825
+ },
29826
+ { session, projection: { name: 1 } }
29827
+ ).toArray() : [];
29828
+ const levelNameToId = new Map(
29829
+ existingLevels.map((level) => [
29830
+ normalizeImportKey(level.name),
29831
+ level._id
29832
+ ])
29833
+ );
29834
+ const missingLevels = (levels ?? []).filter(
29835
+ (levelName) => !levelNameToId.has(normalizeImportKey(levelName))
29836
+ );
29837
+ const levelsPayload = missingLevels.map((levelName) => ({
29732
29838
  blockId: buildingId.toString(),
29733
29839
  site: site.toString(),
29734
29840
  name: levelName,
@@ -29741,23 +29847,80 @@ function useBuildingService() {
29741
29847
  if (levelsPayload.length > 0) {
29742
29848
  insertedLevels = await _bulkWriteLevels(levelsPayload, session);
29743
29849
  }
29744
- const levelNameToId = /* @__PURE__ */ new Map();
29745
29850
  if (insertedLevels) {
29746
29851
  insertedLevels.levels.forEach((lvl) => {
29747
- levelNameToId.set(lvl.name, lvl._id);
29852
+ levelNameToId.set(normalizeImportKey(lvl.name), lvl._id);
29748
29853
  });
29749
29854
  }
29750
- const buildingPayload = {
29751
- _id: buildingId,
29752
- site,
29753
- block,
29754
- name,
29755
- levels: insertedLevels ? Object.values(insertedLevels.insertedIds) : []
29756
- };
29757
- const buildingResult = await _add(buildingPayload, session);
29855
+ const levelIds = Array.from(levelNameToId.values());
29856
+ if (existingBuilding) {
29857
+ if (insertedLevels) {
29858
+ await _updateById(
29859
+ buildingId,
29860
+ {
29861
+ levels: [
29862
+ ...existingLevelIds,
29863
+ ...Object.values(insertedLevels.insertedIds)
29864
+ ]
29865
+ },
29866
+ session
29867
+ );
29868
+ }
29869
+ } else {
29870
+ const buildingPayload = {
29871
+ _id: buildingId,
29872
+ site,
29873
+ block,
29874
+ name,
29875
+ levels: levelIds
29876
+ };
29877
+ const buildingResult = await _add(buildingPayload, session);
29878
+ buildingPayloads.push({ ...buildingPayload, _id: buildingResult });
29879
+ }
29880
+ const existingUnits = levelIds.length > 0 ? await buildingUnitsCollection.find(
29881
+ {
29882
+ building: { $in: [buildingId, buildingId.toString()] },
29883
+ level: {
29884
+ $in: [
29885
+ ...levelIds,
29886
+ ...levelIds.map((levelId) => levelId.toString())
29887
+ ]
29888
+ }
29889
+ },
29890
+ { session, projection: { name: 1, level: 1 } }
29891
+ ).toArray() : [];
29892
+ const existingUnitKeys = new Set(
29893
+ existingUnits.map(
29894
+ (unit) => `${unit.level.toString()}::${normalizeImportKey(unit.name)}`
29895
+ )
29896
+ );
29758
29897
  Array.from(unitsByLevel.entries()).forEach(([levelName, units]) => {
29759
- const levelId = levelNameToId.get(levelName);
29898
+ const levelId = levelNameToId.get(normalizeImportKey(levelName));
29899
+ if (!levelId)
29900
+ return;
29760
29901
  units.forEach((unit) => {
29902
+ const unitKey = `${levelId.toString()}::${normalizeImportKey(unit)}`;
29903
+ const unitRows = unitRowsByLevel.get(levelName)?.get(unit) ?? rows;
29904
+ if (existingUnitKeys.has(unitKey)) {
29905
+ unitRows.forEach((row) => {
29906
+ skippedRows.push({
29907
+ row,
29908
+ block,
29909
+ name,
29910
+ reason: `Unit "${unit}" already exists in level "${levelName}".`
29911
+ });
29912
+ });
29913
+ return;
29914
+ }
29915
+ existingUnitKeys.add(unitKey);
29916
+ unitRows.slice(1).forEach((row) => {
29917
+ skippedRows.push({
29918
+ row,
29919
+ block,
29920
+ name,
29921
+ reason: `Unit "${unit}" is duplicated in this spreadsheet for level "${levelName}".`
29922
+ });
29923
+ });
29761
29924
  buildingUnitPayloads.push({
29762
29925
  site: new import_mongodb54.ObjectId(site),
29763
29926
  name: unit,
@@ -29783,9 +29946,17 @@ function useBuildingService() {
29783
29946
  });
29784
29947
  });
29785
29948
  }
29786
- await _bulkAddBuildingUnits(buildingUnitPayloads);
29949
+ if (buildingUnitPayloads.length > 0) {
29950
+ await _bulkAddBuildingUnits(buildingUnitPayloads, session);
29951
+ }
29787
29952
  await session.commitTransaction();
29788
- return { buildingPayloads, buildingUnitPayloads };
29953
+ return {
29954
+ buildingPayloads,
29955
+ buildingUnitPayloads,
29956
+ insertedBuildings: buildingPayloads.length,
29957
+ insertedUnits: buildingUnitPayloads.length,
29958
+ skippedRows
29959
+ };
29789
29960
  } catch (error) {
29790
29961
  await session.abortTransaction();
29791
29962
  throw error;
@@ -29807,6 +29978,19 @@ var import_joi46 = __toESM(require("joi"));
29807
29978
  var import_exceljs = __toESM(require("exceljs"));
29808
29979
  var import_csv_parser = __toESM(require("csv-parser"));
29809
29980
  var import_fs3 = __toESM(require("fs"));
29981
+ function normalizeSpreadsheetHeader(header) {
29982
+ let value = "";
29983
+ if (header && typeof header === "object" && "richText" in header && Array.isArray(header.richText)) {
29984
+ value = header.richText.map((part) => part.text ?? "").join("");
29985
+ } else if (header && typeof header === "object" && "text" in header) {
29986
+ value = String(header.text ?? "");
29987
+ } else if (header && typeof header === "object" && "result" in header) {
29988
+ value = String(header.result ?? "");
29989
+ } else {
29990
+ value = String(header ?? "");
29991
+ }
29992
+ return value.replace(/^\uFEFF/, "").trim().replace(/\(.*\)/, "").trim().toLowerCase();
29993
+ }
29810
29994
  function useBuildingController() {
29811
29995
  const {
29812
29996
  getAll: _getAll,
@@ -30059,12 +30243,14 @@ function useBuildingController() {
30059
30243
  }
30060
30244
  const { originalname, path: path5 } = req.file;
30061
30245
  const lowerName = originalname.toLowerCase();
30246
+ const expectedHeaders = ["block", "name", "level", "unit"];
30247
+ const requiredFields = ["block", "name"];
30062
30248
  const rowSchema = import_joi46.default.object({
30063
30249
  block: import_joi46.default.number().integer().min(0).required(),
30250
+ name: import_joi46.default.string().trim().required(),
30064
30251
  level: import_joi46.default.alternatives().try(import_joi46.default.string(), import_joi46.default.number()).custom((value) => String(value)).optional().allow(null, ""),
30065
30252
  unit: import_joi46.default.alternatives().try(import_joi46.default.string(), import_joi46.default.number()).custom((value) => String(value)).optional().allow(null, ""),
30066
30253
  category: import_joi46.default.string().trim().optional().allow(null, ""),
30067
- name: import_joi46.default.string().trim().optional().allow(null, ""),
30068
30254
  email: import_joi46.default.string().email().lowercase().optional().allow(null, ""),
30069
30255
  phoneNumber: import_joi46.default.string().trim().optional().allow(null, "")
30070
30256
  });
@@ -30095,9 +30281,18 @@ function useBuildingController() {
30095
30281
  return;
30096
30282
  }
30097
30283
  const headerRow = worksheet.getRow(1);
30098
- const headers = (headerRow.values || []).slice(1).map(
30099
- (header) => String(header ?? "").trim().replace(/\(.*\)/, "").trim()
30284
+ const headers = (headerRow.values || []).slice(1).map((header) => normalizeSpreadsheetHeader(header));
30285
+ const missingHeaders = expectedHeaders.filter(
30286
+ (expected) => !headers.some((h) => h === expected)
30100
30287
  );
30288
+ if (missingHeaders.length > 0) {
30289
+ next(
30290
+ new import_node_server_utils89.BadRequestError(
30291
+ `Invalid Excel format. Missing required columns: ${missingHeaders.join(", ")}. Expected columns: ${expectedHeaders.join(", ")}`
30292
+ )
30293
+ );
30294
+ return;
30295
+ }
30101
30296
  worksheet.eachRow((row, rowNumber) => {
30102
30297
  if (rowNumber === 1)
30103
30298
  return;
@@ -30116,10 +30311,30 @@ function useBuildingController() {
30116
30311
  const parsed = [];
30117
30312
  import_fs3.default.createReadStream(path5).pipe(
30118
30313
  (0, import_csv_parser.default)({
30119
- mapHeaders: ({ header }) => header.trim().replace(/\(.*\)/, "").trim()
30314
+ mapHeaders: ({ header }) => normalizeSpreadsheetHeader(header)
30120
30315
  })
30121
- ).on("data", (row) => parsed.push(row)).on("end", () => resolve(parsed)).on("error", reject);
30316
+ ).on("data", (row) => {
30317
+ if (Object.values(row).some(
30318
+ (v) => v !== "" && v !== null && v !== void 0
30319
+ )) {
30320
+ parsed.push(row);
30321
+ }
30322
+ }).on("end", () => resolve(parsed)).on("error", reject);
30122
30323
  });
30324
+ if (rows.length > 0) {
30325
+ const csvHeaders = Object.keys(rows[0]);
30326
+ const missingHeaders = expectedHeaders.filter(
30327
+ (expected) => !csvHeaders.some((h) => h === expected)
30328
+ );
30329
+ if (missingHeaders.length > 0) {
30330
+ next(
30331
+ new import_node_server_utils89.BadRequestError(
30332
+ `Invalid CSV format. Missing required columns: ${missingHeaders.join(", ")}. Expected columns: ${expectedHeaders.join(", ")}`
30333
+ )
30334
+ );
30335
+ return;
30336
+ }
30337
+ }
30123
30338
  } else {
30124
30339
  next(
30125
30340
  new import_node_server_utils89.BadRequestError("Only .xlsx, .xls, or .csv files are allowed.")
@@ -30129,6 +30344,23 @@ function useBuildingController() {
30129
30344
  const validRows = [];
30130
30345
  const invalidRows = [];
30131
30346
  rows.forEach((row, index) => {
30347
+ const emptyCells = Object.entries(row).filter(
30348
+ ([, value2]) => value2 === "" || value2 === null || value2 === void 0
30349
+ ).map(([key]) => key);
30350
+ const emptyRequiredFields = emptyCells.filter(
30351
+ (cell) => requiredFields.includes(cell)
30352
+ );
30353
+ if (emptyRequiredFields.length > 0) {
30354
+ invalidRows.push({
30355
+ row: index + 2,
30356
+ // +2 because header is row 1
30357
+ data: row,
30358
+ errors: [
30359
+ `Required field(s) empty: ${emptyRequiredFields.join(", ")}`
30360
+ ]
30361
+ });
30362
+ return;
30363
+ }
30132
30364
  const { error, value } = rowSchema.validate(row, {
30133
30365
  abortEarly: false,
30134
30366
  stripUnknown: true
@@ -30141,19 +30373,64 @@ function useBuildingController() {
30141
30373
  errors: error.details.map((d) => d.message)
30142
30374
  });
30143
30375
  } else {
30144
- validRows.push(value);
30376
+ validRows.push({ ...value, __rowNumber: index + 2 });
30145
30377
  }
30146
30378
  });
30147
- await _uploadSpreadsheetBuilding(validRows, site);
30379
+ let result;
30380
+ try {
30381
+ result = await _uploadSpreadsheetBuilding(validRows, site);
30382
+ } catch (uploadError) {
30383
+ import_node_server_utils89.logger.log({
30384
+ level: "error",
30385
+ message: `Upload error: ${uploadError.message}`
30386
+ });
30387
+ result = {
30388
+ insertedBuildings: 0,
30389
+ insertedUnits: 0,
30390
+ skippedRows: [
30391
+ {
30392
+ rowNumber: "all",
30393
+ reason: "Critical Upload Error",
30394
+ error: uploadError.message
30395
+ }
30396
+ ]
30397
+ };
30398
+ }
30399
+ const allSkippedItems = [
30400
+ ...invalidRows.map((item) => ({
30401
+ rowNumber: item.row,
30402
+ reason: "Validation Error",
30403
+ errors: item.errors,
30404
+ data: item.data
30405
+ })),
30406
+ ...(result.skippedRows || []).map((item) => ({
30407
+ rowNumber: "row" in item ? item.row : item.rowNumber,
30408
+ reason: item.reason,
30409
+ block: "block" in item ? item.block : void 0,
30410
+ name: "name" in item ? item.name : void 0,
30411
+ data: item
30412
+ }))
30413
+ ].sort((a, b) => {
30414
+ if (typeof a.rowNumber === "string")
30415
+ return 1;
30416
+ if (typeof b.rowNumber === "string")
30417
+ return -1;
30418
+ return a.rowNumber - b.rowNumber;
30419
+ });
30420
+ const skippedSummary = allSkippedItems.map((item, idx) => `${idx + 1}. Row ${item.rowNumber}: ${item.reason}`).join("\n");
30148
30421
  res.status(200).json({
30149
- message: "Spreadsheet import completed (buildings).",
30422
+ message: `Spreadsheet import completed (buildings).${allSkippedItems.length > 0 ? "\n\nSkipped items:\n" + skippedSummary : ""}`,
30150
30423
  fileName: originalname,
30151
30424
  totalRows: rows.length,
30152
30425
  validRows: validRows.length,
30153
30426
  invalidRows: invalidRows.length,
30427
+ insertedBuildings: result.insertedBuildings,
30428
+ insertedUnits: result.insertedUnits,
30429
+ skippedRows: allSkippedItems.length,
30154
30430
  validationErrors: invalidRows,
30155
- data: validRows
30156
- // testing: return the validated building rows directly
30431
+ skippedErrors: result.skippedRows,
30432
+ skippedItemsSummary: allSkippedItems,
30433
+ data: result
30157
30434
  });
30158
30435
  import_fs3.default.unlink(path5, () => {
30159
30436
  });