-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathItemMasterManager.gs
More file actions
527 lines (443 loc) · 14.9 KB
/
Copy pathItemMasterManager.gs
File metadata and controls
527 lines (443 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
/**
* ============================================================================
* 工廠環境衛生點檢管理系統 - 點檢項目主檔管理模組
* Factory Environmental Hygiene Inspection System - Item Master Manager
* ============================================================================
*
* @file ItemMasterManager.gs
* @version v3.0
* @date 2026-02-14
* @author System Developer
* @description 點檢項目主檔動態管理、版本控制、修訂歷史追蹤
*
* @functions
* - syncItemMaster(): 同步項目主檔到UI
* - validateItemMaster(): 驗證項目主檔資料完整性
* - getItemsByArea(): 依區域取得項目清單
* - saveItemHistory(): 儲存項目修訂歷史
* - compareItemVersion(): 比較項目版本差異
* - rollbackItem(): 回復至指定版本
* - exportItemMaster(): 匯出項目主檔
* - getActiveItems(): 取得啟用中的項目清單
* - updateItemMasterVersion(): 更新項目主檔版本號
*
* @dependencies
* - SheetInitializer.gs (SHEETS常數)
* - InspectionItems.gs (原始項目資料)
*
* @changelog
* v3.0 (2026-02-14) - 項目主檔管理系統建立
* - 動態項目管理
* - 版本控制機制
* - 修訂歷史追蹤
*
* ============================================================================
*/
/**
* 同步點檢項目主檔到UI
* 從Sheet 2讀取項目資料並更新快取
*/
function syncItemMaster() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.ITEM_MASTER);
if (!sheet) {
throw new Error('找不到點檢項目主檔工作表 (Item_Master)');
}
// 讀取項目資料
const data = sheet.getDataRange().getValues();
if (data.length <= 1) {
throw new Error('點檢項目主檔無資料(只有標題列)');
}
const items = [];
const errors = [];
// Sheet 2 欄位索引(v3.0 新增題型和單位):
// A(0): 項目ID
// B(1): 區域
// C(2): 項目編號
// D(3): 項目名稱(中)
// E(4): 項目名稱(英)
// F(5): 題型(選擇題/填空題)
// G(6): 單位(℃、%RH等)
// H(7): 是否啟用
// 跳過標題列,從第2列開始
for (let i = 1; i < data.length; i++) {
const row = data[i];
// 跳過完全空白的列
if (!row[0] && !row[1] && !row[2] && !row[3]) {
continue;
}
// 只處理啟用的項目
if (row[7] === true || row[7] === 'TRUE' || row[7] === '是' || row[7] === 'true') {
const questionType = row[5] ? row[5].toString().trim() : '選擇題';
const item = {
itemId: row[0] ? row[0].toString().trim() : '',
area: row[1] ? row[1].toString().trim() : '',
itemNo: row[2],
nameCh: row[3] ? row[3].toString().trim() : '',
nameEn: row[4] ? row[4].toString().trim() : '',
questionType: questionType, // 題型
unit: row[6] ? row[6].toString().trim() : '', // 單位
isEnabled: true,
lastModified: new Date(),
modifier: Session.getActiveUser().getEmail(),
version: 1.0
};
// 基本驗證
if (!item.itemId) {
errors.push(`第 ${i+1} 列:缺少項目ID`);
}
if (!item.area) {
errors.push(`第 ${i+1} 列:缺少區域`);
}
if (!item.nameCh) {
errors.push(`第 ${i+1} 列:缺少中文名稱`);
}
// 驗證題型
if (questionType !== '選擇題' && questionType !== '填空題') {
errors.push(`第 ${i+1} 列:題型必須是「選擇題」或「填空題」`);
}
// 填空題必須要有單位
if (questionType === '填空題' && !item.unit) {
errors.push(`第 ${i+1} 列:填空題必須填寫單位(如:℃、%RH)`);
}
items.push(item);
}
}
// 如果有錯誤,拋出異常
if (errors.length > 0) {
throw new Error('資料驗證失敗:\n' + errors.join('\n'));
}
// 如果沒有啟用的項目
if (items.length === 0) {
throw new Error('沒有啟用的項目\n\n請確認:\n1. Sheet 2 至少有一列資料\n2. H欄「是否啟用」= TRUE');
}
// 驗證資料
if (!validateItemMaster(items)) {
throw new Error('點檢項目主檔資料驗證失敗(請查看執行記錄)');
}
// 儲存修訂歷史
saveItemHistory(items, '同步點檢項目主檔', Session.getActiveUser().getEmail());
// 更新版本資訊
updateItemMasterVersion();
logSystem('點檢項目主檔同步成功,共 ' + items.length + ' 個啟用項目', 'INFO');
return {
success: true,
count: items.length,
version: getItemMasterVersion(),
timestamp: new Date()
};
} catch (error) {
logSystem('同步點檢項目主檔失敗:' + error.message, 'ERROR');
return {
success: false,
error: error.message
};
}
}
/**
* 驗證點檢項目主檔資料完整性
*/
function validateItemMaster(items) {
try {
if (!items || items.length === 0) {
logSystem('點檢項目主檔為空', 'WARNING');
return false;
}
const areaCount = {};
const duplicateCheck = {};
const errors = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 檢查必填欄位
if (!item.itemId) {
errors.push(`第 ${i+1} 項:缺少項目ID`);
}
if (!item.area) {
errors.push(`第 ${i+1} 項:缺少區域`);
}
if (!item.nameCh) {
errors.push(`第 ${i+1} 項:缺少中文名稱`);
}
// 檢查重複的項目ID
if (item.itemId && duplicateCheck[item.itemId]) {
errors.push(`第 ${i+1} 項:重複的項目ID: ${item.itemId}`);
}
if (item.itemId) {
duplicateCheck[item.itemId] = true;
}
// 統計各區域項目數
if (item.area) {
areaCount[item.area] = (areaCount[item.area] || 0) + 1;
}
}
// 如果有錯誤,記錄並返回false
if (errors.length > 0) {
logSystem('點檢項目主檔驗證失敗:\n' + errors.join('\n'), 'ERROR');
return false;
}
logSystem('點檢項目主檔驗證通過,共 ' + items.length + ' 項,區域分布:' + JSON.stringify(areaCount), 'INFO');
return true;
} catch (error) {
logSystem('驗證點檢項目主檔時發生錯誤:' + error.message, 'ERROR');
return false;
}
}
/**
* 依區域取得項目清單
*/
function getItemsByArea(areaName) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.ITEM_MASTER);
if (!sheet) {
return [];
}
const data = sheet.getDataRange().getValues();
const items = [];
// Sheet 2 欄位:項目ID, 區域, 項目編號, 中文名稱, 英文名稱, 題型, 單位, 是否啟用
for (let i = 1; i < data.length; i++) {
const row = data[i];
// 符合區域且已啟用 (row[7] 是「是否啟用」)
if (row[1] === areaName && (row[7] === true || row[7] === 'TRUE' || row[7] === '是' || row[7] === 'true')) {
items.push({
itemId: row[0],
area: row[1],
itemNo: row[2],
nameCh: row[3],
nameEn: row[4],
questionType: row[5] ? row[5].toString().trim() : '選擇題',
unit: row[6] ? row[6].toString().trim() : ''
});
}
}
// 依項目編號排序
items.sort((a, b) => a.itemNo - b.itemNo);
return items;
} catch (error) {
logSystem('取得區域項目時發生錯誤:' + error.message, 'ERROR');
return [];
}
}
/**
* 儲存項目修訂歷史
*/
function saveItemHistory(items, reason, modifier) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(SHEETS.ITEM_HISTORY);
if (!sheet) {
logSystem('項目修訂歷史工作表不存在,跳過儲存', 'WARNING');
return false;
}
const historyId = 'HIST-' + Utilities.formatDate(new Date(), 'GMT+8', 'yyyyMMdd-HHmmss');
const timestamp = new Date();
const version = getItemMasterVersion();
// 儲存整體快照
const snapshot = JSON.stringify(items);
sheet.appendRow([
historyId,
'SYNC', // 項目ID (同步時用SYNC)
timestamp,
modifier,
'同步',
'', // 修訂前內容
snapshot, // 修訂後內容
reason,
version
]);
logSystem('項目修訂歷史已儲存:' + historyId, 'INFO');
return true;
} catch (error) {
logSystem('儲存項目修訂歷史失敗:' + error.message, 'ERROR');
return false;
}
}
/**
* 取得啟用中的項目清單(提供給UI使用)
*/
function getActiveItems() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.ITEM_MASTER);
if (!sheet) {
// 如果項目主檔不存在,使用預設項目清單
logSystem('項目主檔不存在,使用預設項目清單', 'WARNING');
return getAllInspectionItems();
}
const data = sheet.getDataRange().getValues();
// 如果項目主檔為空或只有標題,使用預設項目清單
if (data.length <= 1) {
logSystem('點檢項目主檔為空,使用預設項目清單', 'WARNING');
return getAllInspectionItems();
}
// 按區域分組
const areaMap = {};
// Sheet 2 欄位:項目ID(0), 區域(1), 項目編號(2), 中文名稱(3), 英文名稱(4), 題型(5), 單位(6), 是否啟用(7)
for (let i = 1; i < data.length; i++) {
const row = data[i];
// 只處理啟用的項目 (row[7] 是「是否啟用」)
if (row[7] === true || row[7] === 'TRUE' || row[7] === '是' || row[7] === 'true') {
const area = row[1];
const questionType = row[5] ? row[5].toString().trim() : '選擇題';
if (!areaMap[area]) {
areaMap[area] = {
code: getAreaCode(area),
name_zh: area,
name_en: getAreaNameEn(area),
items: []
};
}
areaMap[area].items.push({
no: row[2],
name_zh: row[3],
name_en: row[4],
type: questionType === '填空題' ? 'input' : 'choice', // 轉換為前端識別的類型
questionType: questionType, // 保留原始題型
unit: row[6] ? row[6].toString().trim() : '' // 單位
});
}
}
// 轉換為陣列並排序
const areas = Object.values(areaMap);
// 依區域順序排序
const areaOrder = [
'2F倉儲區', '研發實驗室', 'B1區', '更衣消毒室', '緩衝區',
'包裝區', '打漿區', '前處理區', '搥漿區', '蒸煮區', '其他'
];
areas.sort((a, b) => {
return areaOrder.indexOf(a.name_zh) - areaOrder.indexOf(b.name_zh);
});
// 每個區域內的項目依編號排序
areas.forEach(area => {
area.items.sort((a, b) => a.no - b.no);
});
logSystem('成功載入 ' + areas.length + ' 個區域的項目資料', 'INFO');
return areas;
} catch (error) {
logSystem('取得啟用項目時發生錯誤:' + error.message + ',使用預設項目清單', 'ERROR');
return getAllInspectionItems();
}
}
/**
* 取得區域代碼
*/
function getAreaCode(areaName) {
const codeMap = {
'2F倉儲區': 'AREA_2F',
'研發實驗室': 'AREA_RD',
'B1區': 'AREA_B1',
'更衣消毒室': 'AREA_LOCKER',
'緩衝區': 'AREA_BUFFER',
'包裝區': 'AREA_PACKING',
'打漿區': 'AREA_MIXING',
'前處理區': 'AREA_PREP',
'搥漿區': 'AREA_POUNDING',
'蒸煮區': 'AREA_COOKING',
'其他': 'AREA_OTHER'
};
return codeMap[areaName] || 'AREA_UNKNOWN';
}
/**
* 取得區域英文名稱
*/
function getAreaNameEn(areaName) {
const nameMap = {
'2F倉儲區': '2F Storage Area',
'研發實驗室': 'R&D Laboratory',
'B1區': 'B1 Area',
'更衣消毒室': 'Changing & Disinfection Room',
'緩衝區': 'Buffer Zone',
'包裝區': 'Packing Area',
'打漿區': 'Mixing Area',
'前處理區': 'Preparation Area',
'搥漿區': 'Pounding Area',
'蒸煮區': 'Cooking Area',
'其他': 'Others'
};
return nameMap[areaName] || areaName;
}
/**
* 更新項目主檔版本號
*/
function updateItemMasterVersion() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.SYSTEM_CONFIG);
if (!sheet) {
return false;
}
const data = sheet.getDataRange().getValues();
const now = new Date();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === '項目主檔版本') {
const currentVersion = parseFloat(data[i][1]) || 1.0;
const newVersion = (currentVersion + 0.1).toFixed(1);
sheet.getRange(i + 1, 2).setValue(newVersion);
} else if (data[i][0] === '最後同步時間') {
sheet.getRange(i + 1, 2).setValue(Utilities.formatDate(now, 'GMT+8', 'yyyy-MM-dd HH:mm:ss'));
}
}
return true;
} catch (error) {
logSystem('更新項目主檔版本失敗:' + error.message, 'ERROR');
return false;
}
}
/**
* 取得當前項目主檔版本
*/
function getItemMasterVersion() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.SYSTEM_CONFIG);
if (!sheet) {
return '1.0';
}
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === '項目主檔版本') {
return data[i][1] || '1.0';
}
}
return '1.0';
} catch (error) {
return '1.0';
}
}
/**
* 匯出項目主檔
*/
function exportItemMaster() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEETS.ITEM_MASTER);
if (!sheet) {
throw new Error('找不到項目主檔工作表');
}
const data = sheet.getDataRange().getValues();
const version = getItemMasterVersion();
const timestamp = Utilities.formatDate(new Date(), 'GMT+8', 'yyyyMMdd_HHmmss');
// 建立新的試算表作為匯出檔案
const exportSs = SpreadsheetApp.create('項目主檔_v' + version + '_' + timestamp);
const exportSheet = exportSs.getActiveSheet();
// 複製資料
for (let i = 0; i < data.length; i++) {
exportSheet.appendRow(data[i]);
}
const url = exportSs.getUrl();
logSystem('項目主檔已匯出:' + url, 'INFO');
return {
success: true,
url: url,
version: version,
timestamp: timestamp
};
} catch (error) {
logSystem('匯出項目主檔失敗:' + error.message, 'ERROR');
return {
success: false,
error: error.message
};
}
}