ICS2 / apps-script /Code.gs
stat2025's picture
Upload Code.gs
14492d3 verified
Raw
History Blame Contribute Delete
12.3 kB
const CONFIG = Object.freeze({
folderId: "1QBsYtQtUp5-uhqjDRugvxHZinozIu6YY",
spreadsheetId: "1_LiYMMphsJaRRJje0pxKrNSiC1ahBS3GnHM01vPrrCk",
sheetName: "الورقة1",
timezone: "Asia/Riyadh",
maxPhotos: 3,
maxPhotoBytes: 3 * 1024 * 1024,
});
const HEADERS = [
"رقم التوثيق",
"تاريخ ووقت التسجيل",
"اسم الباحث/ة",
"اسم المنشأة",
"السجل التجاري",
"رقم العقد",
"المدينة الصناعية",
"حالة التوثيق",
"الإفادة الميدانية",
"روابط الصور",
"رابط مجلد المنشأة",
"عدد الصور",
"مفتاح العينة",
"آخر تعديل",
];
function doGet(event) {
try {
const params = (event && event.parameter) || {};
const action = String(params.action || "health");
if (action === "health") {
return jsonResponse({ ok: true, service: "ICS2 Documentation", version: 4 });
}
if (action === "status") {
validateResearcherAccess(params.accessCode);
return jsonResponse({
ok: true,
generatedAt: new Date().toISOString(),
records: getDocumentationRecords(true),
});
}
if (action === "admin") {
validateSupervisorAccess(params.accessCode);
return jsonResponse({
ok: true,
generatedAt: new Date().toISOString(),
timezone: CONFIG.timezone,
records: getDocumentationRecords(true),
});
}
throw new Error("الطلب غير معروف.");
} catch (error) {
return jsonResponse({ ok: false, error: String(error.message || error) });
}
}
function authorizeService() {
DriveApp.getFolderById(CONFIG.folderId).getName();
SpreadsheetApp.openById(CONFIG.spreadsheetId).getName();
return "AUTHORIZED";
}
function doPost(event) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(20000);
const request = JSON.parse(event.postData.contents || "{}");
validateResearcherAccess(request.accessCode);
validateRequest(request);
const sheet = getSheet();
ensureHeaders(sheet);
if (request.action === "updateEvidence") {
return jsonResponse(updateEvidence(sheet, request));
}
return jsonResponse(createEvidence(sheet, request));
} catch (error) {
return jsonResponse({ ok: false, error: String(error.message || error) });
} finally {
try {
lock.releaseLock();
} catch (_) {}
}
}
function createEvidence(sheet, request) {
const existingIds = sheet
.getRange(1, 1, Math.max(sheet.getLastRow(), 1), 1)
.getDisplayValues()
.flat();
if (existingIds.includes(request.documentationId)) {
return { ok: true, duplicate: true, documentationId: request.documentationId };
}
const now = new Date();
const folder = getEvidenceFolder(request);
const photoUrls = request.photos.map((photo, index) => savePhoto(folder, photo, request, index));
sheet.appendRow([
request.documentationId,
now,
safeText(request.researcher),
safeText(request.establishmentName),
safeText(request.commercialRecord),
safeText(request.contractNumber) || "لا يوجد رقم عقد",
safeText(request.city),
safeText(request.fieldStatus),
safeText(request.statement),
photoUrls.join("\n"),
folder.getUrl(),
photoUrls.length,
safeText(request.sampleKey),
now,
]);
return evidenceResponse(request, folder.getUrl(), photoUrls, now, now);
}
function updateEvidence(sheet, request) {
const rowNumber = findDocumentationRow(sheet, request.documentationId);
if (!rowNumber) throw new Error("تعذر العثور على التوثيق المطلوب تعديله.");
const row = sheet.getRange(rowNumber, 1, 1, HEADERS.length).getValues()[0];
if (
safeText(row[12]) &&
safeText(row[12]) !== safeText(request.sampleKey) &&
safeText(row[4]) !== safeText(request.commercialRecord)
) {
throw new Error("لا يمكن تعديل توثيق عينة أخرى.");
}
const oldPhotoUrls = splitUrls(row[9]);
const keptPhotoUrls = Array.isArray(request.keptPhotoUrls)
? request.keptPhotoUrls.map(safeText).filter((url) => oldPhotoUrls.includes(url))
: oldPhotoUrls;
if (keptPhotoUrls.length + request.photos.length > CONFIG.maxPhotos) {
throw new Error("الحد الأعلى 3 صور.");
}
const folder = row[10] ? getFolderByUrl(row[10], request) : getEvidenceFolder(request);
const newPhotoUrls = request.photos.map((photo, index) =>
savePhoto(folder, photo, request, keptPhotoUrls.length + index),
);
const updatedAt = new Date();
const photoUrls = [...keptPhotoUrls, ...newPhotoUrls];
sheet.getRange(rowNumber, 8, 1, 7).setValues([[
safeText(request.fieldStatus),
safeText(request.statement),
photoUrls.join("\n"),
folder.getUrl(),
photoUrls.length,
safeText(request.sampleKey),
updatedAt,
]]);
oldPhotoUrls
.filter((url) => !keptPhotoUrls.includes(url))
.forEach(trashDriveFileByUrl);
const documentedAt = row[1] instanceof Date ? row[1] : updatedAt;
return evidenceResponse(request, folder.getUrl(), photoUrls, documentedAt, updatedAt, true);
}
function evidenceResponse(request, folderUrl, photoUrls, documentedAt, updatedAt, updated) {
return {
ok: true,
updated: Boolean(updated),
documentationId: request.documentationId,
sampleKey: safeText(request.sampleKey),
researcher: safeText(request.researcher),
establishmentName: safeText(request.establishmentName),
commercialRecord: safeText(request.commercialRecord),
contractNumber: safeText(request.contractNumber),
city: safeText(request.city),
fieldStatus: safeText(request.fieldStatus),
statement: safeText(request.statement),
documentedAt: documentedAt.toISOString(),
documentedDate: Utilities.formatDate(documentedAt, CONFIG.timezone, "yyyy-MM-dd"),
updatedAt: updatedAt.toISOString(),
folderUrl,
photoUrls,
photoCount: photoUrls.length,
};
}
function findDocumentationRow(sheet, documentationId) {
if (sheet.getLastRow() < 2) return 0;
const ids = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getDisplayValues().flat();
const index = ids.indexOf(String(documentationId));
return index < 0 ? 0 : index + 2;
}
function getFolderByUrl(url, request) {
const id = driveIdFromUrl(url);
return id ? DriveApp.getFolderById(id) : getEvidenceFolder(request);
}
function trashDriveFileByUrl(url) {
const id = driveIdFromUrl(url);
if (!id) return;
try {
DriveApp.getFileById(id).setTrashed(true);
} catch (_) {}
}
function driveIdFromUrl(url) {
const value = String(url || "");
const match =
value.match(/\/folders\/([a-zA-Z0-9_-]+)/) ||
value.match(/\/d\/([a-zA-Z0-9_-]+)/) ||
value.match(/[?&]id=([a-zA-Z0-9_-]+)/);
return match ? match[1] : "";
}
function validateResearcherAccess(accessCode) {
const expected = PropertiesService.getScriptProperties().getProperty("ACCESS_CODE") || "20302030";
if (String(accessCode || "") !== expected) throw new Error("غير مصرح بالوصول.");
}
function validateSupervisorAccess(accessCode) {
const expected = PropertiesService.getScriptProperties().getProperty("SUPERVISOR_CODE") || "1448";
if (String(accessCode || "") !== expected) throw new Error("غير مصرح بالوصول.");
}
function validateRequest(request) {
if (!request.documentationId) throw new Error("رقم التوثيق مفقود.");
if (!safeText(request.sampleKey)) throw new Error("مفتاح العينة مفقود.");
if (!safeText(request.researcher)) throw new Error("اسم الباحث مفقود.");
if (!safeText(request.establishmentName)) throw new Error("اسم المنشأة مفقود.");
if (!safeText(request.fieldStatus)) throw new Error("حالة التوثيق مطلوبة.");
if (!safeText(request.statement)) throw new Error("الإفادة الميدانية مطلوبة.");
if (!Array.isArray(request.photos)) request.photos = [];
if (request.photos.length > CONFIG.maxPhotos) throw new Error("الحد الأعلى 3 صور.");
}
function getDocumentationRecords(includeDetails) {
const sheet = getSheet();
ensureHeaders(sheet);
const lastRow = sheet.getLastRow();
if (lastRow < 2) return [];
const values = sheet.getRange(2, 1, lastRow - 1, HEADERS.length).getValues();
return values
.filter((row) => row.some((cell) => cell !== ""))
.map((row) => {
const date = row[1] instanceof Date ? row[1] : new Date(row[1]);
const validDate = !Number.isNaN(date.getTime());
const base = {
documentationId: safeText(row[0]),
documentedAt: validDate ? date.toISOString() : "",
documentedDate: validDate ? Utilities.formatDate(date, CONFIG.timezone, "yyyy-MM-dd") : "",
researcher: safeText(row[2]),
establishmentName: safeText(row[3]),
commercialRecord: safeText(row[4]),
contractNumber: safeText(row[5]),
city: safeText(row[6]),
fieldStatus: safeText(row[7]),
photoCount: Number(row[11]) || splitUrls(row[9]).length,
sampleKey: safeText(row[12]),
updatedAt: row[13] instanceof Date ? row[13].toISOString() : "",
};
if (!includeDetails) return base;
return Object.assign(base, {
statement: safeText(row[8]),
photoUrls: splitUrls(row[9]),
folderUrl: safeText(row[10]),
});
})
.sort((a, b) =>
String(b.updatedAt || b.documentedAt).localeCompare(String(a.updatedAt || a.documentedAt)),
);
}
function splitUrls(value) {
return safeText(value).split(/\s+/).filter((item) => /^https?:\/\//.test(item));
}
function getSheet() {
const spreadsheet = SpreadsheetApp.openById(CONFIG.spreadsheetId);
return spreadsheet.getSheetByName(CONFIG.sheetName) || spreadsheet.insertSheet(CONFIG.sheetName);
}
function ensureHeaders(sheet) {
const current = sheet.getRange(1, 1, 1, HEADERS.length).getDisplayValues()[0];
if (current.join("") === "") {
sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
sheet.setFrozenRows(1);
sheet.getRange(1, 1, 1, HEADERS.length)
.setFontWeight("bold")
.setBackground("#4137A8")
.setFontColor("#FFFFFF");
sheet.autoResizeColumns(1, HEADERS.length);
return;
}
if (!current[12]) sheet.getRange(1, 13).setValue(HEADERS[12]);
if (!current[13]) sheet.getRange(1, 14).setValue(HEADERS[13]);
}
function getEvidenceFolder(request) {
const root = DriveApp.getFolderById(CONFIG.folderId);
const researcherFolder = getOrCreateFolder(root, safeFileName(request.researcher));
const cityFolder = getOrCreateFolder(researcherFolder, safeFileName(request.city || "مدينة غير محددة"));
const entityName = `${safeText(request.commercialRecord) || "دون سجل"} - ${safeText(request.establishmentName)}`;
return getOrCreateFolder(cityFolder, safeFileName(entityName));
}
function getOrCreateFolder(parent, name) {
const folders = parent.getFoldersByName(name);
return folders.hasNext() ? folders.next() : parent.createFolder(name);
}
function savePhoto(folder, photo, request, index) {
const mimeType = String(photo.mimeType || "");
if (!/^image\/(jpeg|png|webp)$/.test(mimeType)) throw new Error("نوع الصورة غير مدعوم.");
const bytes = Utilities.base64Decode(String(photo.base64 || ""));
if (bytes.length > CONFIG.maxPhotoBytes) throw new Error("إحدى الصور أكبر من الحد المسموح.");
const extension = mimeType === "image/png" ? "png" : mimeType === "image/webp" ? "webp" : "jpg";
const timestamp = Utilities.formatDate(new Date(), CONFIG.timezone, "yyyyMMdd-HHmmss");
const name = safeFileName(
`${request.commercialRecord || "دون-سجل"}-${timestamp}-${index + 1}.${extension}`,
);
return folder.createFile(Utilities.newBlob(bytes, mimeType, name)).getUrl();
}
function safeText(value) {
return String(value == null ? "" : value).trim().slice(0, 2000);
}
function safeFileName(value) {
return safeText(value).replace(/[\\/:*?"<>|#%{}[\]]/g, "-").slice(0, 140) || "غير محدد";
}
function jsonResponse(payload) {
return ContentService.createTextOutput(JSON.stringify(payload))
.setMimeType(ContentService.MimeType.JSON);
}