@shisyamo4131/air-guard-v2-schemas 1.3.1-dev.8 → 2.0.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/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as Agreement } from "./src/Agreement.js";
2
2
  export { default as ArrangementNotification } from "./src/ArrangementNotification.js";
3
+ export { default as Billing } from "./src/Billing.js";
3
4
  export { default as Company } from "./src/Company.js";
4
5
  export { default as Customer, CustomerMinimal } from "./src/Customer.js";
5
6
  export { default as CutoffDate } from "./src/utils/CutoffDate.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shisyamo4131/air-guard-v2-schemas",
3
- "version": "1.3.1-dev.8",
3
+ "version": "2.0.0",
4
4
  "description": "Schemas for AirGuard V2",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -139,7 +139,6 @@ import {
139
139
  VALUES,
140
140
  OPTIONS,
141
141
  } from "./constants/arrangement-notification-status.js";
142
- import { runTransaction } from "firebase/firestore";
143
142
 
144
143
  const classProps = {
145
144
  status: defField("arrangementNotificationStatus", { required: true }),
@@ -431,6 +430,14 @@ export default class ArrangementNotification extends SiteOperationScheduleDetail
431
430
  arguments: { id },
432
431
  };
433
432
  try {
433
+ // サーバー側での実行を禁止
434
+ if (this.type === "SERVER") {
435
+ throw new Error(
436
+ "fetchDocsBySiteOperationScheduleId is not supported on server side. " +
437
+ "Please use this method only on client side or implement server-specific logic with explicit prefix handling."
438
+ );
439
+ }
440
+
434
441
  const instance = new ArrangementNotification();
435
442
  const constraints = [["where", "siteOperationScheduleId", "==", id]];
436
443
  const result = await instance.fetchDocs({ constraints });
@@ -459,6 +466,14 @@ export default class ArrangementNotification extends SiteOperationScheduleDetail
459
466
  arguments: { ...options, transaction },
460
467
  };
461
468
  try {
469
+ // サーバー側での実行を禁止
470
+ if (this.type === "SERVER") {
471
+ throw new Error(
472
+ "bulkDelete is not supported on server side. " +
473
+ "Please use this method only on client side or implement server-specific logic with explicit prefix handling."
474
+ );
475
+ }
476
+
462
477
  const { siteOperationScheduleId, workerIds = [] } = options;
463
478
  if (!siteOperationScheduleId) {
464
479
  throw new Error("siteOperationScheduleId is required");
@@ -495,8 +510,7 @@ export default class ArrangementNotification extends SiteOperationScheduleDetail
495
510
  if (transaction) {
496
511
  await performTransaction(transaction);
497
512
  } else {
498
- const firestore = this.getAdapter().firestore;
499
- await runTransaction(firestore, performTransaction);
513
+ await this.runTransaction(performTransaction);
500
514
  }
501
515
  } catch (error) {
502
516
  throw new ContextualError(error.message, context);
package/src/Billing.js CHANGED
@@ -6,9 +6,8 @@
6
6
  *
7
7
  * @prop {string} customerId - customer document id
8
8
  * @prop {string} siteId - site document id
9
- * @prop {string} billingMonth - billing month (YYYY-MM format)
10
- * @prop {Date} billingDate - billing date
11
- * @prop {Date} paymentDueDate - payment due date
9
+ * @prop {Date} billingDateAt - billing date
10
+ * @prop {Date} paymentDueDateAt - payment due date
12
11
  *
13
12
  * @prop {Array} paymentRecords - payment records (not implemented yet)
14
13
  *
@@ -17,10 +16,14 @@
17
16
  * @prop {Object} adjustment - adjustment
18
17
  * @prop {string} remarks - remarks
19
18
  *
19
+ * @prop {string} billingMonth - billing month (YYYY-MM format) (read-only)
20
+ * @prop {Date} billingDate - billing date (YYYY-MM-DD format) (read-only)
21
+ * @prop {string} paymentDueMonth - payment due month (YYYY-MM format) (read-only)
22
+ * @prop {Date} paymentDueDate - payment due date (YYYY-MM-DD format) (read-only)
20
23
  * @prop {number} subtotal - subtotal (excluding tax) (computed-readonly)
21
24
  * @prop {number} taxAmount - tax amount (computed-readonly)
22
25
  * @prop {number} totalAmount - total amount (including tax) (computed-readonly)
23
- * @prop {Array<Object>} itemsSummary - billing items summary for display (computed-readonly)
26
+ * @prop {Array<Object>} summary - summary for display (computed-readonly)
24
27
  *****************************************************************************/
25
28
 
26
29
  import FireModel from "@shisyamo4131/air-firebase-v2";
@@ -37,9 +40,8 @@ const STATUS = {
37
40
  const classProps = {
38
41
  customerId: defField("customerId", { required: true }),
39
42
  siteId: defField("siteId", { required: true }),
40
- billingMonth: defField("oneLine", { required: true }),
41
- billingDateAt: defField("date"),
42
- paymentDueDateAt: defField("date"),
43
+ billingDateAt: defField("dateAt", { required: true }),
44
+ paymentDueDateAt: defField("dateAt"),
43
45
 
44
46
  // 入金管理用配列(現時点では未使用 将来の拡張用)
45
47
  paymentRecords: defField("array", { default: [] }), // Not implemented yet
@@ -66,6 +68,75 @@ export default class Billing extends FireModel {
66
68
  afterInitialize(item = {}) {
67
69
  super.afterInitialize(item);
68
70
 
71
+ // billingDate (YYYY-MM-DD) と billingMonth (YYYY-MM) の計算用プロパティを定義
72
+ Object.defineProperties(this, {
73
+ billingDate: {
74
+ configurable: true,
75
+ enumerable: true,
76
+ get() {
77
+ if (!this.billingDateAt) return null;
78
+ const jstDate = new Date(
79
+ this.billingDateAt.getTime() + 9 * 60 * 60 * 1000
80
+ ); /* JST補正 */
81
+ const year = jstDate.getUTCFullYear();
82
+ const month = jstDate.getUTCMonth() + 1;
83
+ const day = jstDate.getUTCDate();
84
+ return `${year}-${String(month).padStart(2, "0")}-${String(
85
+ day
86
+ ).padStart(2, "0")}`;
87
+ },
88
+ set(v) {},
89
+ },
90
+ billingMonth: {
91
+ configurable: true,
92
+ enumerable: true,
93
+ get() {
94
+ if (!this.billingDateAt) return null;
95
+ const jstDate = new Date(
96
+ this.billingDateAt.getTime() + 9 * 60 * 60 * 1000
97
+ ); /* JST補正 */
98
+ const year = jstDate.getUTCFullYear();
99
+ const month = jstDate.getUTCMonth() + 1;
100
+ return `${year}-${String(month).padStart(2, "0")}`;
101
+ },
102
+ set(v) {},
103
+ },
104
+ });
105
+
106
+ Object.defineProperties(this, {
107
+ paymentDueDate: {
108
+ configurable: true,
109
+ enumerable: true,
110
+ get() {
111
+ if (!this.paymentDueDateAt) return null;
112
+ const jstDate = new Date(
113
+ this.paymentDueDateAt.getTime() + 9 * 60 * 60 * 1000
114
+ ); /* JST補正 */
115
+ const year = jstDate.getUTCFullYear();
116
+ const month = jstDate.getUTCMonth() + 1;
117
+ const day = jstDate.getUTCDate();
118
+ return `${year}-${String(month).padStart(2, "0")}-${String(
119
+ day
120
+ ).padStart(2, "0")}`;
121
+ },
122
+ set(v) {},
123
+ },
124
+ paymentDueMonth: {
125
+ configurable: true,
126
+ enumerable: true,
127
+ get() {
128
+ if (!this.paymentDueDateAt) return null;
129
+ const jstDate = new Date(
130
+ this.paymentDueDateAt.getTime() + 9 * 60 * 60 * 1000
131
+ ); /* JST補正 */
132
+ const year = jstDate.getUTCFullYear();
133
+ const month = jstDate.getUTCMonth() + 1;
134
+ return `${year}-${String(month).padStart(2, "0")}`;
135
+ },
136
+ set(v) {},
137
+ },
138
+ });
139
+
69
140
  // 小計(税抜)を計算
70
141
  Object.defineProperty(this, "subtotal", {
71
142
  get() {
@@ -82,7 +153,9 @@ export default class Billing extends FireModel {
82
153
  // 消費税額を計算
83
154
  Object.defineProperty(this, "taxAmount", {
84
155
  get() {
85
- return Math.floor(this.subtotal * 0.1); // 10% 切り捨て
156
+ return this.operationResults.reduce((sum, item) => {
157
+ return sum + (item.tax || 0);
158
+ }, 0);
86
159
  },
87
160
  set() {},
88
161
  enumerable: true,
@@ -100,7 +173,7 @@ export default class Billing extends FireModel {
100
173
  });
101
174
 
102
175
  // 表示用の明細サマリーを生成
103
- Object.defineProperty(this, "itemsSummary", {
176
+ Object.defineProperty(this, "summary", {
104
177
  get() {
105
178
  return this.operationResults.map((item) => ({
106
179
  operationResultId: item.docId,
package/src/Customer.js CHANGED
@@ -29,6 +29,10 @@
29
29
  * @static
30
30
  * @prop {string} STATUS_ACTIVE - constant for active contract status
31
31
  * @prop {string} STATUS_TERMINATED - constant for terminated contract status
32
+ *
33
+ * @method getPaymentDueDateAt
34
+ * @param {Date} baseDate - base date in UTC (JST - 9 hours)
35
+ * @returns {Date} payment due date in UTC (JST - 9 hours)
32
36
  *****************************************************************************/
33
37
  import FireModel from "@shisyamo4131/air-firebase-v2";
34
38
  import { defField } from "./parts/fieldDefinitions.js";
@@ -99,6 +103,42 @@ export default class Customer extends FireModel {
99
103
  prefecture: defAccessor("prefecture"),
100
104
  });
101
105
  }
106
+
107
+ /**
108
+ * 支払期日を計算する
109
+ * @param {Date} baseDate - 基準日(JSTから9時間引いたUTC表現)
110
+ * @returns {Date} 支払期日(JSTから9時間引いたUTC表現)
111
+ */
112
+ getPaymentDueDateAt(baseDate) {
113
+ // UTC → JST に変換(+9時間)
114
+ const jstDate = new Date(baseDate.getTime() + 9 * 60 * 60 * 1000);
115
+
116
+ // UTCメソッドでJST相当の年月を取得
117
+ const year = jstDate.getUTCFullYear();
118
+ const month = jstDate.getUTCMonth();
119
+
120
+ // paymentMonth分加算した年月を計算
121
+ const targetMonth = month + this.paymentMonth;
122
+ const targetYear = year + Math.floor(targetMonth / 12);
123
+ const finalMonth = targetMonth % 12;
124
+
125
+ let dueDate;
126
+ if (this.paymentDate === CutoffDate.VALUES.END_OF_MONTH) {
127
+ // 月末の場合
128
+ dueDate = new Date(Date.UTC(targetYear, finalMonth + 1, 0));
129
+ } else {
130
+ // 指定日の場合
131
+ dueDate = new Date(Date.UTC(targetYear, finalMonth, this.paymentDate));
132
+
133
+ // 指定日が存在しない場合は月末にする
134
+ if (dueDate.getUTCMonth() !== finalMonth) {
135
+ dueDate = new Date(Date.UTC(targetYear, finalMonth + 1, 0));
136
+ }
137
+ }
138
+
139
+ // JST → UTC に変換(-9時間)
140
+ return new Date(dueDate.getTime() - 9 * 60 * 60 * 1000);
141
+ }
102
142
  }
103
143
 
104
144
  /*****************************************************************************
@@ -41,7 +41,6 @@
41
41
  * - Setter: Splits array into employees and outsourcers based on `isEmployee` property
42
42
  * @prop {string|null} siteOperationScheduleId - Associated SiteOperationSchedule document ID
43
43
  * - If this OperationResult was created from a SiteOperationSchedule, this property holds that ID.
44
- * - If this property is set, the instance cannot be deleted.
45
44
  * @prop {boolean} useAdjustedQuantity - Flag to indicate if adjusted quantities are used for billing
46
45
  * @prop {number} adjustedQuantityBase - Adjusted quantity for base workers
47
46
  * - Quantity used for billing base workers when `useAdjustedQuantity` is true.
@@ -56,6 +55,10 @@
56
55
  * @prop {boolean} isLocked - Lock flag
57
56
  * - When set to true, the OperationResult is locked from edits exept for editing as OperationBilling.
58
57
  * @prop {Agreement|null} agreement - Associated Agreement object
58
+ * - The Agreement instance associated with this OperationResult for pricing and billing information.
59
+ * - When set, it influences billing calculations such as unit prices and billing dates.
60
+ * @prop {boolean} allowEmptyAgreement - Flag to ignore missing Agreement
61
+ * - When set to true, allows the OperationResult to be valid even if no Agreement is associated.
59
62
  *
60
63
  * @readonly
61
64
  * @prop {string} date - Date string in YYYY-MM-DD format based on `dateAt` (read-only)
@@ -76,9 +79,12 @@
76
79
  * @prop {number} overtimeWorkMinutes - Overtime work in minutes (read-only)
77
80
  * - Calculated as `totalWorkMinutes` minus `regulationWorkMinutes`
78
81
  * @prop {boolean} hasAgreement - Indicates if an Agreement is associated (read-only)
79
- * @prop {boolean} isValid - Indicates if the OperationResult is valid (read-only)
80
- * - Valid if either an Agreement is associated or adjusted quantities are not used.
81
- * - If `hasAgreement` is true, always valid.
82
+ * - `true` if `agreement` is set, otherwise `false`.
83
+ * @prop {string|false} isInvalid - Validation status (read-only)
84
+ * - Returns false if valid.
85
+ * - Returns reason code string if invalid:
86
+ * - `EMPTY_BILLING_DATE`: Billing date is missing.
87
+ * - `EMPTY_AGREEMENT`: Agreement is missing and `allowEmptyAgreement` is false.
82
88
  * @prop {Object} statistics - Statistics of workers (read-only)
83
89
  * - Contains counts and total work minutes for base and qualified workers, including OJT breakdowns.
84
90
  * - Structure: { base: {...}, qualified: {...}, total: {...} }
@@ -95,6 +101,8 @@
95
101
  * - Calculated using the `Tax` utility based on `salesAmount` and `date`.
96
102
  * @prop {number} billingAmount - Total billing amount including tax (read-only)
97
103
  * - Sum of `salesAmount` and `tax`.
104
+ * @prop {string|null} billingDate - Billing date in YYYY-MM-DD format (read-only)
105
+ * - Returns a string in the format YYYY-MM-DD based on `billingDateAt`.
98
106
  * @prop {string} billingMonth - Billing month in YYYY-MM format (read-only)
99
107
  * @prop {Array<string>} employeeIds - Array of employee IDs from `employees` (read-only)
100
108
  * @prop {Array<string>} outsourcerIds - Array of outsourcer IDs from `outsourcers` (read-only)
@@ -123,9 +131,6 @@
123
131
  * @getter {number} endMinute - End minute (0-59) (read-only)
124
132
  * - Extracted from `endTime`.
125
133
  *
126
- * @method beforeDelete - Override method to prevent deletion with siteOperationScheduleId
127
- * - Prevents deletion if the instance has `siteOperationScheduleId`.
128
- * - Throws an error if deletion is attempted on an instance created from SiteOperationSchedule.
129
134
  * @method refreshBillingDateAt - Refresh billingDateAt based on dateAt and cutoffDate
130
135
  * - Updates `billingDateAt` based on the current `dateAt` and `cutoffDate` values.
131
136
  * @method addWorker - Adds a new worker (employee or outsourcer)
@@ -163,12 +168,20 @@
163
168
  * - @param {Object|string} key - The combined key string or object
164
169
  * - @returns {Array<string>} - Array containing [siteId, shiftType, date]
165
170
  * - @throws {Error} - If the key is invalid.
171
+ * @method toggleLock - Toggle the lock status of an OperationResult document
172
+ * - @param {string} docId - Document ID
173
+ * - @param {boolean} value - Lock status value
174
+ * - @returns {Promise<void>}
166
175
  *
167
- * @override create - Override create method to indicate not implemented
176
+ * @override
177
+ * @method create - Override create method to indicate not implemented
168
178
  * - Creation of OperationBilling instances is not implemented, as billing records are typically
169
179
  * generated through the OperationResult class.
180
+ * @method update - Override update method to allow editing even when isLocked is true
181
+ * @method delete - Override delete method to allow deletion even when isLocked is true
170
182
  *****************************************************************************/
171
183
  import OperationResult from "./OperationResult.js";
184
+ import Operation from "./Operation.js";
172
185
 
173
186
  export default class OperationBilling extends OperationResult {
174
187
  static className = "稼働請求";
@@ -182,4 +195,45 @@ export default class OperationBilling extends OperationResult {
182
195
  async create() {
183
196
  return Promise.reject(new Error("[OperationBilling.js] Not implemented."));
184
197
  }
198
+
199
+ /**
200
+ * Override update method to allow editing even when isLocked is true
201
+ * @param {*} options
202
+ * @returns {Promise<void>}
203
+ */
204
+ async update(options = {}) {
205
+ // isLockedのチェックをスキップして、親クラス(Operation)のupdateを直接呼び出す
206
+ return await Operation.prototype.update.call(this, options);
207
+ }
208
+
209
+ /**
210
+ * Override delete method to allow deletion even when isLocked is true
211
+ * @param {*} options
212
+ * @returns {Promise<void>}
213
+ */
214
+ async delete(options = {}) {
215
+ // isLockedのチェックをスキップして、親クラス(Operation)のdeleteを直接呼び出す
216
+ return await Operation.prototype.delete.call(this, options);
217
+ }
218
+
219
+ /**
220
+ * Toggle the lock status of an OperationResult document
221
+ * @param {string} docId - Document ID
222
+ * @param {boolean} value - Lock status value
223
+ */
224
+ static async toggleLock(docId, value) {
225
+ if (!docId || typeof docId !== "string") {
226
+ throw new Error("Invalid docId provided to toggleLock method");
227
+ }
228
+ if (typeof value !== "boolean") {
229
+ throw new Error("Invalid value provided to toggleLock method");
230
+ }
231
+ const instance = new OperationBilling();
232
+ const doc = await instance.fetchDoc({ docId });
233
+ if (!doc) {
234
+ throw new Error(`OperationResult document with ID ${docId} not found`);
235
+ }
236
+ doc.isLocked = value;
237
+ await doc.update();
238
+ }
185
239
  }
@@ -5,7 +5,6 @@
5
5
  *
6
6
  * - Extends Operation class to represent the result of an operation.
7
7
  * - Also incorporates Agreement class properties for pricing and billing information.
8
- * - Prevents deletion if the instance has `siteOperationScheduleId`.
9
8
  * - Provides comprehensive billing calculations including statistics, sales amounts, and tax.
10
9
  * - Supports both daily and hourly billing with adjusted quantities.
11
10
  * - Automatically updates `billingDateAt` based on `dateAt` and `cutoffDate`.
@@ -41,7 +40,6 @@
41
40
  * - Setter: Splits array into employees and outsourcers based on `isEmployee` property
42
41
  * @prop {string|null} siteOperationScheduleId - Associated SiteOperationSchedule document ID
43
42
  * - If this OperationResult was created from a SiteOperationSchedule, this property holds that ID.
44
- * - If this property is set, the instance cannot be deleted.
45
43
  * @prop {boolean} useAdjustedQuantity - Flag to indicate if adjusted quantities are used for billing
46
44
  * @prop {number} adjustedQuantityBase - Adjusted quantity for base workers
47
45
  * - Quantity used for billing base workers when `useAdjustedQuantity` is true.
@@ -56,7 +54,10 @@
56
54
  * @prop {boolean} isLocked - Lock flag
57
55
  * - When set to true, the OperationResult is locked from edits exept for editing as OperationBilling.
58
56
  * @prop {Agreement|null} agreement - Associated Agreement object
59
- *
57
+ * - The Agreement instance associated with this OperationResult for pricing and billing information.
58
+ * - When set, it influences billing calculations such as unit prices and billing dates.
59
+ * @prop {boolean} allowEmptyAgreement - Flag to ignore missing Agreement
60
+ * - When set to true, allows the OperationResult to be valid even if no Agreement is associated.
60
61
  * @readonly
61
62
  * @prop {string} date - Date string in YYYY-MM-DD format based on `dateAt` (read-only)
62
63
  * - Returns a string in the format YYYY-MM-DD based on `dateAt`.
@@ -76,9 +77,12 @@
76
77
  * @prop {number} overtimeWorkMinutes - Overtime work in minutes (read-only)
77
78
  * - Calculated as `totalWorkMinutes` minus `regulationWorkMinutes`
78
79
  * @prop {boolean} hasAgreement - Indicates if an Agreement is associated (read-only)
79
- * @prop {boolean} isValid - Indicates if the OperationResult is valid (read-only)
80
- * - Valid if either an Agreement is associated or adjusted quantities are not used.
81
- * - If `hasAgreement` is true, always valid.
80
+ * - `true` if `agreement` is set, otherwise `false`.
81
+ * @prop {string|false} isInvalid - Validation status (read-only)
82
+ * - Returns false if valid.
83
+ * - Returns reason code string if invalid:
84
+ * - `EMPTY_BILLING_DATE`: Billing date is missing.
85
+ * - `EMPTY_AGREEMENT`: Agreement is missing and `allowEmptyAgreement` is false.
82
86
  * @prop {Object} statistics - Statistics of workers (read-only)
83
87
  * - Contains counts and total work minutes for base and qualified workers, including OJT breakdowns.
84
88
  * - Structure: { base: {...}, qualified: {...}, total: {...} }
@@ -95,6 +99,8 @@
95
99
  * - Calculated using the `Tax` utility based on `salesAmount` and `date`.
96
100
  * @prop {number} billingAmount - Total billing amount including tax (read-only)
97
101
  * - Sum of `salesAmount` and `tax`.
102
+ * @prop {string|null} billingDate - Billing date in YYYY-MM-DD format (read-only)
103
+ * - Returns a string in the format YYYY-MM-DD based on `billingDateAt`.
98
104
  * @prop {string} billingMonth - Billing month in YYYY-MM format (read-only)
99
105
  * @prop {Array<string>} employeeIds - Array of employee IDs from `employees` (read-only)
100
106
  * @prop {Array<string>} outsourcerIds - Array of outsourcer IDs from `outsourcers` (read-only)
@@ -123,9 +129,6 @@
123
129
  * @getter {number} endMinute - End minute (0-59) (read-only)
124
130
  * - Extracted from `endTime`.
125
131
  *
126
- * @method beforeDelete - Override method to prevent deletion with siteOperationScheduleId
127
- * - Prevents deletion if the instance has `siteOperationScheduleId`.
128
- * - Throws an error if deletion is attempted on an instance created from SiteOperationSchedule.
129
132
  * @method refreshBillingDateAt - Refresh billingDateAt based on dateAt and cutoffDate
130
133
  * - Updates `billingDateAt` based on the current `dateAt` and `cutoffDate` values.
131
134
  * @method addWorker - Adds a new worker (employee or outsourcer)
@@ -164,7 +167,11 @@
164
167
  * - @returns {Array<string>} - Array containing [siteId, shiftType, date]
165
168
  * - @throws {Error} - If the key is invalid.
166
169
  *
167
- * @override setDateAtCallback - Updates `billingDateAt` based on the new `dateAt` value.
170
+ * @override
171
+ * @method setDateAtCallback - Updates `billingDateAt` based on the new `dateAt` value.
172
+ * @method beforeCreate - Override to sync customerId from siteId
173
+ * @method beforeUpdate - Override to sync customerId from siteId when siteId changes
174
+ * @method beforeDelete - Override to prevent deletion if isLocked is true
168
175
  *****************************************************************************/
169
176
  import Operation from "./Operation.js";
170
177
  import Agreement from "./Agreement.js";
@@ -175,6 +182,7 @@ import Tax from "./tax.js";
175
182
  import { BILLING_UNIT_TYPE_PER_HOUR } from "./constants/billing-unit-type.js";
176
183
  import RoundSetting from "./RoundSetting.js";
177
184
  import CutoffDate from "./utils/CutoffDate.js";
185
+ import Site from "./Site.js";
178
186
 
179
187
  const classProps = {
180
188
  ...Operation.classProps,
@@ -199,7 +207,7 @@ const classProps = {
199
207
  label: "資格残業(調整)",
200
208
  default: 0,
201
209
  }),
202
- billingDateAt: defField("dateAt", { label: "請求日付" }),
210
+ billingDateAt: defField("dateAt", { label: "請求締日" }),
203
211
  employees: defField("array", { customClass: OperationResultDetail }),
204
212
  outsourcers: defField("array", {
205
213
  customClass: OperationResultDetail,
@@ -209,10 +217,22 @@ const classProps = {
209
217
  default: false,
210
218
  }),
211
219
  agreement: defField("object", { label: "取極め", customClass: Agreement }),
212
- ignoreEmptyAgreement: defField("check", {
213
- label: "取極めなしを無視",
220
+ allowEmptyAgreement: defField("check", {
221
+ label: "取極めなしを許容",
214
222
  default: false,
215
223
  }),
224
+
225
+ /**
226
+ * siteId から自動同期されるプロパティ
227
+ * - 従属する取引先の変更を不可としているため、ドキュメントの更新時に取得するだけで問題ない。
228
+ * - 但し、siteId が変更された時は再取得する必要がある。
229
+ */
230
+ customerId: defField("customerId", { required: true, hidden: true }),
231
+ };
232
+
233
+ const INVALID_REASON = {
234
+ EMPTY_BILLING_DATE: "EMPTY_BILLING_DATE",
235
+ EMPTY_AGREEMENT: "EMPTY_AGREEMENT",
216
236
  };
217
237
 
218
238
  export default class OperationResult extends Operation {
@@ -234,7 +254,6 @@ export default class OperationResult extends Operation {
234
254
  super.afterInitialize();
235
255
 
236
256
  /** Computed properties */
237
- let _agreement = this.agreement;
238
257
  Object.defineProperties(this, {
239
258
  statistics: {
240
259
  configurable: true,
@@ -405,7 +424,7 @@ export default class OperationResult extends Operation {
405
424
  },
406
425
  set(v) {},
407
426
  },
408
- billingMonth: {
427
+ billingDate: {
409
428
  configurable: true,
410
429
  enumerable: true,
411
430
  get() {
@@ -415,21 +434,26 @@ export default class OperationResult extends Operation {
415
434
  ); /* JST補正 */
416
435
  const year = jstDate.getUTCFullYear();
417
436
  const month = jstDate.getUTCMonth() + 1;
418
- return `${year}-${String(month).padStart(2, "0")}`;
437
+ const day = jstDate.getUTCDate();
438
+ return `${year}-${String(month).padStart(2, "0")}-${String(
439
+ day
440
+ ).padStart(2, "0")}`;
419
441
  },
420
442
  set(v) {},
421
443
  },
422
-
423
- agreement: {
444
+ billingMonth: {
424
445
  configurable: true,
425
446
  enumerable: true,
426
447
  get() {
427
- return _agreement;
428
- },
429
- set(v) {
430
- _agreement = v;
431
- this.refreshBillingDateAt();
448
+ if (!this.billingDateAt) return null;
449
+ const jstDate = new Date(
450
+ this.billingDateAt.getTime() + 9 * 60 * 60 * 1000
451
+ ); /* JST補正 */
452
+ const year = jstDate.getUTCFullYear();
453
+ const month = jstDate.getUTCMonth() + 1;
454
+ return `${year}-${String(month).padStart(2, "0")}`;
432
455
  },
456
+ set(v) {},
433
457
  },
434
458
  hasAgreement: {
435
459
  configurable: true,
@@ -439,16 +463,40 @@ export default class OperationResult extends Operation {
439
463
  },
440
464
  set(v) {},
441
465
  },
442
- isValid: {
466
+ isInvalid: {
443
467
  configurable: true,
444
468
  enumerable: true,
445
469
  get() {
446
- if (this.hasAgreement) return true;
447
- return this.ignoreEmptyAgreement;
470
+ if (!this.agreement && !this.allowEmptyAgreement) {
471
+ return INVALID_REASON.EMPTY_AGREEMENT;
472
+ }
473
+ if (!this.billingDateAt) {
474
+ return INVALID_REASON.EMPTY_BILLING_DATE;
475
+ }
476
+ return false;
448
477
  },
449
478
  set(v) {},
450
479
  },
451
480
  });
481
+
482
+ /** Triggers */
483
+ let _agreement = this.agreement;
484
+ Object.defineProperties(this, {
485
+ agreement: {
486
+ configurable: true,
487
+ enumerable: true,
488
+ get() {
489
+ return _agreement;
490
+ },
491
+ set(v) {
492
+ const oldKey = _agreement ? _agreement.key : null;
493
+ const newKey = v ? v.key : null;
494
+ if (oldKey === newKey) return;
495
+ _agreement = v;
496
+ this.refreshBillingDateAt();
497
+ },
498
+ },
499
+ });
452
500
  }
453
501
 
454
502
  /**
@@ -484,50 +532,65 @@ export default class OperationResult extends Operation {
484
532
  }
485
533
 
486
534
  /**
487
- * Override create method to validate billingDateAt when ignoreEmptyAgreement is true
488
- * @param {*} options
489
- * @returns {Promise<DocumentReference>}
535
+ * Synchronize customerId from siteId
536
+ * @returns {Promise<void>}
537
+ * @throws {Error} If the specified siteId does not exist
490
538
  */
491
- async create(options = {}) {
492
- if (this.ignoreEmptyAgreement && !this.billingDateAt) {
539
+ async _syncCustomerId() {
540
+ if (!this.siteId) return;
541
+ const siteInstance = new Site();
542
+ const siteExists = await siteInstance.fetch({ docId: this.siteId });
543
+ if (!siteExists) {
493
544
  throw new Error(
494
- "[OperationResult] Billing date is required when 'ignoreEmptyAgreement' is true."
545
+ `[OperationResult] The specified siteId (${this.siteId}) does not exist.`
495
546
  );
496
547
  }
497
- return await super.create(options);
548
+ this.customerId = siteInstance.customerId;
549
+ }
550
+
551
+ /**
552
+ * Override beforeCreate to sync customerId
553
+ * @returns {Promise<void>}
554
+ */
555
+ async beforeCreate() {
556
+ await super.beforeCreate();
557
+
558
+ // Sync customerId
559
+ await this._syncCustomerId();
498
560
  }
499
561
 
500
562
  /**
501
- * Override update method to prevent editing if isLocked is true
502
- * - Also validate billingDateAt when ignoreEmptyAgreement is true
503
- * @param {*} options
563
+ * Override beforeUpdate to sync customerId if siteId changed
504
564
  * @returns {Promise<void>}
505
565
  */
506
- async update(options = {}) {
566
+ async beforeUpdate() {
567
+ await super.beforeUpdate();
568
+
569
+ // Prevent editing if isLocked is true
507
570
  if (this.isLocked) {
508
571
  throw new Error(
509
572
  "[OperationResult] This OperationResult is locked and cannot be edited."
510
573
  );
511
574
  }
512
- if (this.ignoreEmptyAgreement && !this.billingDateAt) {
513
- throw new Error(
514
- "[OperationResult] Billing date is required when 'ignoreEmptyAgreement' is true."
515
- );
516
- }
517
- return await super.update(options);
575
+
576
+ // Sync customerId if siteId changed
577
+ if (this.siteId === this._beforeData.siteId && this.customerId) return;
578
+ await this._syncCustomerId();
518
579
  }
519
580
 
520
581
  /**
521
- * Override delete method to prevent deletion if isLocked is true
522
- * @param {*} options
582
+ * Override beforeDelete to prevent deletion if isLocked is true
523
583
  * @returns {Promise<void>}
584
+ * @throws {Error} If isLocked is true
524
585
  */
525
- async delete(options = {}) {
586
+ async beforeDelete() {
587
+ await super.beforeDelete();
588
+
589
+ // Prevent deletion if isLocked is true
526
590
  if (this.isLocked) {
527
591
  throw new Error(
528
592
  "[OperationResult] This OperationResult is locked and cannot be deleted."
529
593
  );
530
594
  }
531
- return await super.delete(options);
532
595
  }
533
596
  }
package/src/Site.js CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * @file src/Site.js
3
3
  * @author shisyamo4131
4
- * @version 1.0.0
4
+ * @version 1.1.0
5
+ * @update 2025-11-20 version 0.2.0-bata
6
+ * - Prevent changing customer reference on update.
7
+ * - Move `customer` property to the top of classProps for better visibility.
5
8
  */
6
9
  import { default as FireModel } from "@shisyamo4131/air-firebase-v2";
7
10
  import { defField } from "./parts/fieldDefinitions.js";
@@ -12,6 +15,16 @@ import Agreement from "./Agreement.js";
12
15
  import { VALUES } from "./constants/site-status.js";
13
16
 
14
17
  const classProps = {
18
+ customer: defField("customer", {
19
+ required: true,
20
+ customClass: CustomerMinimal,
21
+ component: {
22
+ attrs: {
23
+ api: () => fetchDocsApi(CustomerMinimal),
24
+ noFilter: true,
25
+ },
26
+ },
27
+ }),
15
28
  code: defField("code", { label: "現場コード" }),
16
29
  name: defField("name", {
17
30
  label: "現場名",
@@ -29,16 +42,6 @@ const classProps = {
29
42
  address: defField("address", { required: true }),
30
43
  building: defField("building"),
31
44
  location: defField("location"),
32
- customer: defField("customer", {
33
- required: true,
34
- customClass: CustomerMinimal,
35
- component: {
36
- attrs: {
37
- api: () => fetchDocsApi(CustomerMinimal),
38
- noFilter: true,
39
- },
40
- },
41
- }),
42
45
  remarks: defField("multipleLine", { label: "備考" }),
43
46
  agreements: defField("array", { label: "取極め", customClass: Agreement }),
44
47
  status: defField("siteStatus", { required: true }),
@@ -115,6 +118,19 @@ export default class Site extends FireModel {
115
118
  static STATUS_ACTIVE = VALUES.ACTIVE.value;
116
119
  static STATUS_TERMINATED = VALUES.TERMINATED.value;
117
120
 
121
+ /**
122
+ * Override beforeUpdate to prevent changing customer reference.
123
+ * @returns {Promise<void>}
124
+ */
125
+ async beforeUpdate() {
126
+ await super.beforeUpdate();
127
+ if (this.customer.docId !== this._beforeData.customer.docId) {
128
+ return Promise.reject(
129
+ new Error("Not allowed to change customer reference.")
130
+ );
131
+ }
132
+ }
133
+
118
134
  afterInitialize(item = {}) {
119
135
  super.afterInitialize(item);
120
136
 
@@ -210,6 +210,18 @@ export const fieldDefinitions = {
210
210
  label: "市区町村",
211
211
  length: 10,
212
212
  },
213
+ customerId: {
214
+ ...generalDefinitions.oneLine,
215
+ label: "取引先",
216
+ component: {
217
+ name: "air-autocomplete-api",
218
+ attrs: {
219
+ itemValue: "docId",
220
+ itemTitle: "name",
221
+ noFilter: true,
222
+ },
223
+ },
224
+ },
213
225
  displayName: {
214
226
  ...generalDefinitions.oneLine,
215
227
  label: "表示名",