utiller 1.0.394 → 1.0.396

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.
@@ -699,6 +699,156 @@ var Utiller = /*#__PURE__*/function () {
699
699
  var factor = Math.pow(10, precision);
700
700
  return Math.ceil(a * b * factor) / factor;
701
701
  });
702
+ /**
703
+ * 根據課程資訊生成 Google Calendar 的行事曆新增連結。
704
+ * @param {object} event - 行程物件。
705
+ * @param {string} event.title - 行程主題 (e.g., '吉他課')。
706
+ * @param {string} event.startDate - 開始日期 (e.g., '2025/11/10')。
707
+ * @param {string} event.startTime - 開始時間 (e.g., '14:00')。
708
+ * @param {string} event.endDate - 結束日期 (e.g., '2025/11/10')。
709
+ * @param {string} event.endTime - 結束時間 (e.g., '16:00')。
710
+ * @param {string} event.location - 地點/地址 (e.g., '台北市大安區信義路三段 162 號')。
711
+ * @param {string} event.details - 描述/備註 (e.g., '請攜帶吉他,並預習第5頁。')。
712
+ * @returns {string} Google Calendar 的新增連結。
713
+ */
714
+ (0, _defineProperty2["default"])(this, "generateGoogleCalendarLink", function (_ref4) {
715
+ var title = _ref4.title,
716
+ startDate = _ref4.startDate,
717
+ startTime = _ref4.startTime,
718
+ endDate = _ref4.endDate,
719
+ endTime = _ref4.endTime,
720
+ location = _ref4.location,
721
+ details = _ref4.details;
722
+ // 使用 moment 結合日期和時間,並格式化為 YYYYMMDDTHHMMSS
723
+ // 注意: 日期格式已從 YYYY-MM-DD 變更為 YYYY/MM/DD
724
+ var startDateTime = (0, _momentTimezone["default"])("".concat(startDate, " ").concat(startTime), "YYYY/MM/DD HH:mm").format("YYYYMMDDTHHMMSS");
725
+ var endDateTime = (0, _momentTimezone["default"])("".concat(endDate, " ").concat(endTime), "YYYY/MM/DD HH:mm").format("YYYYMMDDTHHMMSS");
726
+
727
+ // 進行 URL 編碼
728
+ var encodedTitle = encodeURIComponent(title);
729
+ var encodedLocation = encodeURIComponent(location);
730
+ var encodedDetails = encodeURIComponent(details);
731
+
732
+ // --- Google Calendar 連結 (使用 https://calendar.google.com/calendar/r/eventedit) ---
733
+ var googleCalendarLink = "https://calendar.google.com/calendar/r/eventedit?" + "text=".concat(encodedTitle) + "&dates=".concat(startDateTime, "/").concat(endDateTime) + "&details=".concat(encodedDetails) + "&location=".concat(encodedLocation) + "&ctz=Asia/Taipei" + // 台灣時區
734
+ "&trp=true"; // 設為忙碌 (Busy)
735
+
736
+ return googleCalendarLink;
737
+ });
738
+ /**
739
+ * 根據課程資訊生成 TimeTree 的行事曆新增連結。
740
+ * (此函式不需要 moment.js,但維持其結構)
741
+ * @param {object} event - 行程物件。
742
+ * @param {string} event.title - 行程主題 (e.g., '吉他課')。
743
+ * @param {string} event.startDate - 開始日期 (YYYY/MM/DD, e.g., '2025/11/10')。
744
+ * @param {string} event.startTime - 開始時間 (HH:MM, e.g., '14:00')。
745
+ * @param {string} event.endDate - 結束日期 (YYYY/MM/DD, e.g., '2025/11/10')。
746
+ * @param {string} event.endTime - 結束時間 (HH:MM, e.g., '16:00')。
747
+ * @param {string} [event.location=''] - 地點/地址 (選填)。
748
+ * @param {string} [event.memo=''] - 描述/備註 (選填,TimeTree 使用 'memo')。
749
+ * @returns {string} TimeTree 行事曆新增連結。
750
+ */
751
+ (0, _defineProperty2["default"])(this, "generateTimeTreeLink", function (_ref5) {
752
+ var title = _ref5.title,
753
+ startDate = _ref5.startDate,
754
+ startTime = _ref5.startTime,
755
+ endDate = _ref5.endDate,
756
+ endTime = _ref5.endTime,
757
+ _ref5$location = _ref5.location,
758
+ location = _ref5$location === void 0 ? "" : _ref5$location,
759
+ _ref5$memo = _ref5.memo,
760
+ memo = _ref5$memo === void 0 ? "" : _ref5$memo;
761
+ // 進行 URL 編碼
762
+ var encodedTitle = encodeURIComponent(title);
763
+ var encodedLocation = encodeURIComponent(location);
764
+ var encodedMemo = encodeURIComponent(memo);
765
+
766
+ // TimeTree 網址結構相對簡單,直接使用模板字串
767
+ // 注意: TimeTree 連結直接使用 YYYY-MM-DD 格式,故此函式仍傳遞原始 YYYY/MM/DD 字串
768
+ // 在實際應用中,如果 TimeTree 不接受 YYYY/MM/DD,則需要在傳入前轉換 (但 TimeTree 連結通常較為寬鬆)
769
+ var timeTreeLink = "https://timetreeapp.com/plans/new?" + "title=".concat(encodedTitle) + "&start_date=".concat(startDate.replace(/\//g, "-")) + // 將 YYYY/MM/DD 轉換回 YYYY-MM-DD
770
+ "&start_time=".concat(startTime) + "&end_date=".concat(endDate.replace(/\//g, "-")) + // 將 YYYY/MM/DD 轉換回 YYYY-MM-DD
771
+ "&end_time=".concat(endTime) + "".concat(location ? "&location=".concat(encodedLocation) : "") + "".concat(memo ? "&memo=".concat(encodedMemo) : "");
772
+ return timeTreeLink;
773
+ });
774
+ /**
775
+ * 根據課程資訊生成 iCalendar (.ics) 檔案內容。
776
+ * @param {object} event - 行程物件。
777
+ * @param {string} event.title - 行程主題。
778
+ * @param {string} event.startDate - 開始日期 (YYYY/MM/DD)。
779
+ * @param {string} event.startTime - 開始時間 (HH:MM)。
780
+ * @param {string} event.endDate - 結束日期 (YYYY/MM/DD)。
781
+ * @param {string} event.endTime - 結束時間 (HH:MM)。
782
+ * @param {string} event.location - 地點/地址。
783
+ * @param {string} event.details - 描述/備註。
784
+ * @returns {string} 遵循 iCalendar 規範的字串內容。
785
+ */
786
+ (0, _defineProperty2["default"])(this, "generateIcsContent", function (_ref6) {
787
+ var title = _ref6.title,
788
+ startDate = _ref6.startDate,
789
+ startTime = _ref6.startTime,
790
+ endDate = _ref6.endDate,
791
+ endTime = _ref6.endTime,
792
+ location = _ref6.location,
793
+ details = _ref6.details;
794
+ // 使用 moment-timezone 結合日期和時間,並格式化為 YYYYMMDDTHHMMSS
795
+ // 注意: 日期格式已從 YYYY-MM-DD 變更為 YYYY/MM/DD
796
+ var startDateTime = _momentTimezone["default"].tz("".concat(startDate, " ").concat(startTime), "YYYY/MM/DD HH:mm", "Asia/Taipei").format("YYYYMMDDTHHMMSS");
797
+ var endDateTime = _momentTimezone["default"].tz("".concat(endDate, " ").concat(endTime), "YYYY/MM/DD HH:mm", "Asia/Taipei").format("YYYYMMDDTHHMMSS");
798
+
799
+ // DTSTAMP 必須是 UTC 格式 (以 Z 結尾)
800
+ var stamp = (0, _momentTimezone["default"])().utc().format("YYYYMMDDTHHMMSS") + "Z";
801
+
802
+ // 唯一識別碼 (UID)
803
+ var uid = Date.now().toString(36) + Math.random().toString(36).substring(2, 5) + "@gemini-app.com";
804
+
805
+ // 確保內容中的特殊字元(如換行、逗號、分號)被正確逸出
806
+ var escapeIcs = function escapeIcs(text) {
807
+ return text.replace(/\\/g, "\\\\").replace(/,/g, "\\,").replace(/;/g, "\\;").replace(/\n/g, "\\n");
808
+ };
809
+ var escapedTitle = escapeIcs(title);
810
+ var escapedLocation = escapeIcs(location);
811
+ var escapedDetails = escapeIcs(details);
812
+
813
+ // 產生 ICS 內容
814
+ var icsContent = "BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + "PRODID:-//Gemini AI//NONSGML v1.0//EN\n" + "BEGIN:VEVENT\n" + "UID:".concat(uid, "\n") + "DTSTAMP:".concat(stamp, "\n") + // 採用 Asia/Taipei 時區
815
+ "DTSTART;TZID=Asia/Taipei:".concat(startDateTime, "\n") + "DTEND;TZID=Asia/Taipei:".concat(endDateTime, "\n") + "SUMMARY:".concat(escapedTitle, "\n") + "LOCATION:".concat(escapedLocation, "\n") + "DESCRIPTION:".concat(escapedDetails, "\n") + "END:VEVENT\n" + "END:VCALENDAR";
816
+
817
+ // 確保使用 CRLF (\r\n) 換行符號,這是 ICS 規範的要求
818
+ return icsContent.replace(/\n/g, "\r\n");
819
+ });
820
+ /**
821
+ * 根據行程物件,生成所有主要的行事曆新增連結 (Google, TimeTree, iCalendar/ICS)。
822
+ * @param {object} event - 行程物件。
823
+ * @param {string} event.title - 行程主題。
824
+ * @param {string} event.startDate - 開始日期 (YYYY/MM/DD)。
825
+ * @param {string} event.startTime - 開始時間 (HH:MM)。
826
+ * @param {string} event.endDate - 結束日期 (YYYY/MM/DD)。
827
+ * @param {string} event.endTime - 結束時間 (HH:MM)。
828
+ * @param {string} event.location - 地點/地址。
829
+ * @param {string} event.details - 描述/備註。
830
+ * @returns {{google: string, timeTree: string, ics: string}} 包含所有連結的物件。
831
+ */
832
+ (0, _defineProperty2["default"])(this, "generateAllCalendarLinks", function (event) {
833
+ // 1. Google Calendar Link
834
+ var googleLink = _this.generateGoogleCalendarLink(event);
835
+
836
+ // 2. TimeTree Link
837
+ // TimeTree 使用 'memo' 參數,因此將 event.details 映射到 memo
838
+ var timeTreeEvent = _objectSpread(_objectSpread({}, event), {}, {
839
+ memo: event.details
840
+ });
841
+ var timeTreeLink = _this.generateTimeTreeLink(timeTreeEvent);
842
+
843
+ // 3. iCalendar (ICS) Link (Data URI 格式,用於 iOS/Outlook/下載)
844
+ var icsContent = _this.generateIcsContent(event);
845
+ var icsLink = "data:text/calendar;charset=utf8,".concat(encodeURIComponent(icsContent));
846
+ return {
847
+ google: googleLink,
848
+ timeTree: timeTreeLink,
849
+ ics: icsLink // iCalendar 連結
850
+ };
851
+ });
702
852
  /**
703
853
  * @param {Array<Object>} current - 要更新的目標陣列
704
854
  * @param {Array<Object>} reference - 用來替換屬性的參照陣列
@@ -3987,9 +4137,9 @@ var Utiller = /*#__PURE__*/function () {
3987
4137
  key: "mutateRemoveKeys",
3988
4138
  value: function mutateRemoveKeys(array, keysToRemove) {
3989
4139
  _lodash["default"].forEach(array, function (obj, index) {
3990
- var filtered = Object.fromEntries(Object.entries(obj).filter(function (_ref5) {
3991
- var _ref6 = (0, _slicedToArray2["default"])(_ref5, 1),
3992
- key = _ref6[0];
4140
+ var filtered = Object.fromEntries(Object.entries(obj).filter(function (_ref8) {
4141
+ var _ref9 = (0, _slicedToArray2["default"])(_ref8, 1),
4142
+ key = _ref9[0];
3993
4143
  return !keysToRemove.includes(key);
3994
4144
  }));
3995
4145
  // 原地替換每個 object 的 key
@@ -4016,9 +4166,9 @@ var Utiller = /*#__PURE__*/function () {
4016
4166
  key: "removeKeysFromArrayObjects",
4017
4167
  value: function removeKeysFromArrayObjects(array, keysToRemove) {
4018
4168
  return _lodash["default"].map(array, function (obj) {
4019
- return Object.fromEntries(Object.entries(obj).filter(function (_ref7) {
4020
- var _ref8 = (0, _slicedToArray2["default"])(_ref7, 1),
4021
- key = _ref8[0];
4169
+ return Object.fromEntries(Object.entries(obj).filter(function (_ref10) {
4170
+ var _ref11 = (0, _slicedToArray2["default"])(_ref10, 1),
4171
+ key = _ref11[0];
4022
4172
  return !keysToRemove.includes(key);
4023
4173
  }));
4024
4174
  });
@@ -4077,10 +4227,10 @@ var Utiller = /*#__PURE__*/function () {
4077
4227
  var predict = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (attr) {
4078
4228
  return attr.checked !== true;
4079
4229
  };
4080
- return _lodash["default"].fromPairs(_lodash["default"].toPairs(obj).filter(function (_ref9) {
4081
- var _ref10 = (0, _slicedToArray2["default"])(_ref9, 2),
4082
- _ = _ref10[0],
4083
- value = _ref10[1];
4230
+ return _lodash["default"].fromPairs(_lodash["default"].toPairs(obj).filter(function (_ref12) {
4231
+ var _ref13 = (0, _slicedToArray2["default"])(_ref12, 2),
4232
+ _ = _ref13[0],
4233
+ value = _ref13[1];
4084
4234
  return predict(value);
4085
4235
  }));
4086
4236
  }
@@ -4243,6 +4393,11 @@ var Utiller = /*#__PURE__*/function () {
4243
4393
  value: function isFirestoreAutoId(id) {
4244
4394
  return _lodash["default"].isString(id) && id.length === 20 && /^[A-Za-z0-9]{20}$/.test(id);
4245
4395
  }
4396
+ }, {
4397
+ key: "getAutoIdOfFirestore",
4398
+ value: function getAutoIdOfFirestore() {
4399
+ return this.getRandomHashV2(20);
4400
+ }
4246
4401
  }, {
4247
4402
  key: "getStringOfConvertTimeRange",
4248
4403
  value:
@@ -4271,9 +4426,9 @@ var Utiller = /*#__PURE__*/function () {
4271
4426
  }, {
4272
4427
  key: "getTSOfSpecificDate",
4273
4428
  value: function getTSOfSpecificDate(dateStr) {
4274
- var _ref11 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
4275
- _ref11$end = _ref11.end,
4276
- end = _ref11$end === void 0 ? false : _ref11$end;
4429
+ var _ref14 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
4430
+ _ref14$end = _ref14.end,
4431
+ end = _ref14$end === void 0 ? false : _ref14$end;
4277
4432
  return Number((0, _lodash["default"])(dateStr).thru(function (str) {
4278
4433
  return (0, _momentTimezone["default"])(str.split("(")[0], "YYYY/MM/DD");
4279
4434
  }).thru(function (m) {
@@ -4389,15 +4544,14 @@ var Utiller = /*#__PURE__*/function () {
4389
4544
  * 🧩 範例:
4390
4545
  * ```js
4391
4546
  * const array = [
4392
- * { a: { c: 1 } },
4393
- * { b: { d: 2 } },
4394
- * { a: { f: 3 } }
4547
+ * { a: { b: { c: 2 } } },
4548
+ * { b: { d: { g: 1 } }, a: { b: { y: 1 }, h: { e: 1 } } }
4395
4549
  * ];
4396
4550
  *
4397
4551
  * ArrayHelper.mergeArrayByKey(array);
4398
4552
  *
4399
4553
  * console.log(array);
4400
- * // 👉 [ { a: { c: 1, f: 3 } }, { b: { d: 2 } } ]
4554
+ * // 👉 [ { a: { b: { c: 2, y: 1 }, h: { e: 1 } } }, { b: { d: { g: 1 } } } ]
4401
4555
  * ```
4402
4556
  *
4403
4557
  * @param {Array<Object>} array - 需被合併的物件陣列
@@ -4406,21 +4560,65 @@ var Utiller = /*#__PURE__*/function () {
4406
4560
  function mergeArrayByKey(array) {
4407
4561
  if (!Array.isArray(array)) return array;
4408
4562
 
4409
- // 1️⃣ 利用 lodash 深層合併所有物件(例如 {a:{x}} + {a:{y}} => {a:{x,y}})
4410
- var merged = _lodash["default"].merge.apply(_lodash["default"], [{}].concat((0, _toConsumableArray2["default"])(array)));
4563
+ // 收集所有 key 的合併結果
4564
+ var resultMap = {};
4565
+ var _iterator20 = _createForOfIteratorHelper(array),
4566
+ _step20;
4567
+ try {
4568
+ for (_iterator20.s(); !(_step20 = _iterator20.n()).done;) {
4569
+ var obj = _step20.value;
4570
+ if (!_lodash["default"].isPlainObject(obj)) continue;
4571
+ for (var _i16 = 0, _Object$entries = Object.entries(obj); _i16 < _Object$entries.length; _i16++) {
4572
+ var _Object$entries$_i = (0, _slicedToArray2["default"])(_Object$entries[_i16], 2),
4573
+ _key45 = _Object$entries$_i[0],
4574
+ value = _Object$entries$_i[1];
4575
+ if (!resultMap[_key45]) {
4576
+ resultMap[_key45] = _lodash["default"].cloneDeep(value);
4577
+ } else {
4578
+ _lodash["default"].merge(resultMap[_key45], value);
4579
+ }
4580
+ }
4581
+ }
4411
4582
 
4412
- // 2️⃣ 清空原陣列(mutate)
4583
+ // 清空原陣列(mutate)
4584
+ } catch (err) {
4585
+ _iterator20.e(err);
4586
+ } finally {
4587
+ _iterator20.f();
4588
+ }
4413
4589
  array.length = 0;
4414
4590
 
4415
- // 3️⃣ 將合併結果重新拆成 [{a:{...}}, {b:{...}}] 結構
4416
- Object.entries(merged).forEach(function (_ref12) {
4417
- var _ref13 = (0, _slicedToArray2["default"])(_ref12, 2),
4418
- key = _ref13[0],
4419
- value = _ref13[1];
4591
+ // 重建結構
4592
+ Object.entries(resultMap).forEach(function (_ref15) {
4593
+ var _ref16 = (0, _slicedToArray2["default"])(_ref15, 2),
4594
+ key = _ref16[0],
4595
+ value = _ref16[1];
4420
4596
  array.push((0, _defineProperty2["default"])({}, key, value));
4421
4597
  });
4422
4598
  return array;
4423
4599
  }
4600
+ }, {
4601
+ key: "getFilteredHeraPeriods",
4602
+ value:
4603
+ // 範例用法 (請確保 moment.js 已載入)
4604
+ /*
4605
+ const myEvent = {
4606
+ title: '吉他課 (Moment.js)',
4607
+ startDate: '2026/01/15', // 使用新的 YYYY/MM/DD 格式
4608
+ startTime: '10:30',
4609
+ endDate: '2026/01/15',
4610
+ endTime: '12:00',
4611
+ location: '新北市板橋區縣民大道一段 1 號',
4612
+ details: 'Moment.js 測試:請記得帶譜架。'
4613
+ };
4614
+ if (typeof moment !== 'undefined') {
4615
+ const links = generateAllCalendarLinks(myEvent);
4616
+ console.log('--- 使用 Moment.js 生成的連結 ---');
4617
+ console.log(links);
4618
+ } else {
4619
+ console.warn('moment.js 函式庫未載入,無法執行。');
4620
+ }
4621
+ */
4424
4622
 
4425
4623
  /** ============== 排課系統公式 開始 ============== */
4426
4624
 
@@ -4439,9 +4637,7 @@ var Utiller = /*#__PURE__*/function () {
4439
4637
  * 2. idOfBooze和idOfVariant為PK, 重複的只保留一組
4440
4638
  * 3. 回傳filter array,(反查出哪些課程重複會用到其他資訊)
4441
4639
  * */
4442
- }, {
4443
- key: "getFilteredHeraPeriods",
4444
- value: function getFilteredHeraPeriods(arr, idOfCurrentBooze) {
4640
+ function getFilteredHeraPeriods(arr, idOfCurrentBooze) {
4445
4641
  return _lodash["default"].chain(arr)
4446
4642
  // 1️⃣ 刪掉 idOfBooze 等於目標值的項目
4447
4643
  .filter(function (item) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utiller",
3
- "version": "1.0.394",
3
+ "version": "1.0.396",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
13
  "configerer": "^1.0.23",
14
- "utiller": "^1.0.393",
14
+ "utiller": "^1.0.395",
15
15
  "linepayer": "^1.0.15",
16
16
  "databazer": "^1.0.17",
17
17
  "lodash": "^4.17.20",