204 lines
7.5 KiB
HTML
204 lines
7.5 KiB
HTML
/// <reference path="../pb_data/types.d.ts" />
|
|
const XLSX = require('xlsx');
|
|
const axios = require('axios');
|
|
|
|
// ================================================
|
|
// CONFIG — ONLY EDIT THESE VALUES
|
|
// ================================================
|
|
const EXCEL_FILE_URL = "https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/JOBS%201000-9999.xlsx?d=wc20d9b5023f84403b4d345ca88382eb4&csf=1&web=1&e=aNwcPv";
|
|
const EXCEL_SHEET_NAME = "Job Sheet";
|
|
|
|
const AZURE_TENANT_ID = "3fd97ea7-b124-41f1-855f-52d8ac3b16c7";
|
|
const AZURE_CLIENT_ID = "3c846e71-9609-40e1-b458-0eb805e21b9f";
|
|
const AZURE_CLIENT_SECRET = "lG~8Q~kKp5nyc-SK8THNE4ZLTdp81zv_gVm2jdm~"; // Store in Settings → Secret keys (see below)
|
|
|
|
// Run every 5 minutes
|
|
routerAdd("GET", "/_cron/sync-excel", (c) => {
|
|
const correlationId = $security.randomString(8);
|
|
console.log(`[${new Date().toISOString()}] Sync started (${correlationId})`);
|
|
|
|
// Use secret storage (secure!)
|
|
const clientSecret = $app.settings().meta.appSecret || AZURE_CLIENT_SECRET;
|
|
|
|
// ================================================
|
|
// 1. Get OAuth 2.0 token (client credentials flow)
|
|
// ================================================
|
|
$os.exec("curl", "-X", "POST",
|
|
`https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`,
|
|
"-d", `client_id=${AZURE_CLIENT_ID}`,
|
|
"-d", `client_secret=${clientSecret}`,
|
|
"-d", "grant_type=client_credentials",
|
|
"-d", "scope=https://graph.microsoft.com/.default"
|
|
).then(async (tokenRes) => {
|
|
const tokenData = JSON.parse(tokenRes.stdout);
|
|
const accessToken = tokenData.access_token;
|
|
|
|
// ================================================
|
|
// 2. Download Excel file
|
|
// ================================================
|
|
const fileRes = await $http.send({
|
|
url: EXCEL_FILE_URL + "?:@microsoft.graph.downloadUrl",
|
|
method: "GET",
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
|
|
if (fileRes.statusCode !== 200) {
|
|
console.error("Failed to download Excel:", fileRes.raw);
|
|
return c.json(500, { error: "Download failed" });
|
|
}
|
|
|
|
const workbook = XLSX.read(fileRes.raw, { type: "buffer", cellDates: true });
|
|
const sheet = workbook.Sheets[EXCEL_SHEET_NAME];
|
|
if (!sheet) return console.error("Sheet not found");
|
|
|
|
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
|
const merges = sheet["!merges"] || [];
|
|
|
|
const getValue = (r, c) => {
|
|
const addr = XLSX.utils.encode_cell({ r, c });
|
|
const cell = sheet[addr];
|
|
if (cell) return cell.v ?? cell.w ?? null;
|
|
for (const m of merges) {
|
|
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
|
|
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
|
|
return master ? (master.v ?? master.w ?? null) : null;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// ================================================
|
|
// 3. Parse headers & data (same as browser tool)
|
|
// ================================================
|
|
const HEADER_ROW = 2;
|
|
const DATA_START = 3;
|
|
|
|
const rawHeaders = [];
|
|
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
let h = getValue(HEADER_ROW, c);
|
|
rawHeaders.push(h ? String(h).trim() : '');
|
|
}
|
|
|
|
const HEADER_MAP = {
|
|
"Job_Number": "Job_Number",
|
|
"Job_Full_Name": "Job_Full_Name",
|
|
"Job_Name": "Job_Name",
|
|
"Job Address": "Job_Address",
|
|
"Job Type": "Job_Type",
|
|
"Flag": "Flag",
|
|
"Job Status": "Job_Status",
|
|
"Job Division": "Division",
|
|
"Estimator": "Estimator",
|
|
"Office Rep": "Office_Rep",
|
|
"Manager": "Project_Manager",
|
|
"Company/Client": "Company_Client",
|
|
"Contact Person": "Contact_Person",
|
|
"Phone Number": "Phone",
|
|
"Email": "Email",
|
|
"Due Date": "Due_Date",
|
|
"Due Date Source": "Due_Date_Source",
|
|
"Due Date Counter": "Due_Date_Counter",
|
|
"Due Time": "Due_Time",
|
|
"Docs Uploaded": "Docs_Uploaded",
|
|
"QB Created": "QB_Created",
|
|
"Added to Calendar": "Calendar_Added",
|
|
"PSwift Uploaded": "PSwift_Uploaded",
|
|
"Voxer Created": "Voxer_Created",
|
|
"Estimate Reviewed": "Estimate_Reviewed",
|
|
"Estimate Draft": "Estimate_Draft",
|
|
"Estimate Approved": "Estimate_Approved",
|
|
"Start Date": "Start_Date",
|
|
"Schedule\nConfidence": "Schedule_Confidence",
|
|
"Notes": "Notes",
|
|
"Job QB Link": "Job_QB_Link",
|
|
"Voxer Link": "Voxer_Link",
|
|
"Job_Codes": "Job_Codes",
|
|
"Submission Date": "Submission_Date",
|
|
"Active": "Active",
|
|
"Job Folder Link": "Job_Folder_Link",
|
|
"Tax Exempt": "Tax_Exempt",
|
|
"pb_id": "id"
|
|
};
|
|
|
|
const rows = [];
|
|
for (let r = DATA_START; r <= range.e.r; r++) {
|
|
const row = { id: null, Notes: null, Job_Number: null };
|
|
let hasData = false;
|
|
|
|
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
const excelHeader = rawHeaders[c - range.s.c];
|
|
const pbField = HEADER_MAP[excelHeader];
|
|
if (!pbField) continue;
|
|
|
|
let val = getValue(r, c);
|
|
if (val !== null && val !== undefined) {
|
|
const s = String(val).trim();
|
|
if (s === '' || /^n\/?a$/i.test(s) || s === '#N/A') val = null;
|
|
}
|
|
|
|
if (pbField === "id" && excelHeader === "pb_id") row.id = val;
|
|
else if (pbField === "Notes") row.Notes = val;
|
|
else if (pbField === "Job_Number") row.Job_Number = val;
|
|
|
|
row[pbField] = val;
|
|
if (val !== null && val !== '') hasData = true;
|
|
}
|
|
|
|
if (hasData) rows.push(row);
|
|
}
|
|
|
|
// ================================================
|
|
// 4. Full Sync Excel → PocketBase
|
|
// ================================================
|
|
let created = 0, updated = 0;
|
|
for (const row of rows) {
|
|
try {
|
|
if (row.id) {
|
|
// Update existing
|
|
const cleanRow = { ...row };
|
|
delete cleanRow.id;
|
|
$app.dao().runInTransaction((txDao) => {
|
|
txDao.saveRecord(txDao.findRecordById("Job_Info_TestEnv", row.id, cleanRow));
|
|
});
|
|
updated++;
|
|
} else {
|
|
// Create new
|
|
const record = $app.dao().newRecord("Job_Info_TestEnv");
|
|
Object.assign(record, row);
|
|
$app.dao().saveRecord(record);
|
|
created++;
|
|
}
|
|
} catch (err) {
|
|
console.error(`Row error (Job_Number ${row.Job_Number}):`, err.message);
|
|
}
|
|
}
|
|
|
|
// ================================================
|
|
// 5. Bidirectional Notes Sync (PB → Excel if PB is newer)
|
|
// ================================================
|
|
const pbRecords = arrayOf(new Record({
|
|
collection: $app.dao().findCollectionByNameOrId("Job_Info_TestEnv")
|
|
})).fill($app.dao().findRecordsByExpr("Job_Info_TestEnv", DB().and()));
|
|
|
|
for (const pbRec of pbRecords) {
|
|
const excelMatch = rows.find(r => r.Job_Number === pbRec.getString("Job_Number"));
|
|
if (!excelMatch) continue;
|
|
|
|
const pbNotes = pbRec.getString("Notes") || "";
|
|
const excelNotes = excelMatch.Notes || "";
|
|
|
|
if (pbNotes !== excelNotes && pbRec.getDate("updated") > new Date()) {
|
|
// Push PB → Excel
|
|
console.log(`Pushing Notes to Excel for ${pbRec.getString("Job_Number")}`);
|
|
// Future: Use Graph workbook API to update cell
|
|
// For now: just log (add Graph PATCH later)
|
|
}
|
|
}
|
|
|
|
console.log(`Sync complete: ${created} created, ${updated} updated`);
|
|
return c.json(200, { status: "success", created, updated });
|
|
}).catch(err => {
|
|
console.error("Sync failed:", err);
|
|
return c.json(500, { error: err.message });
|
|
});
|
|
}, null, { cron: "*/5 * * * *" }); // Every 5 minutes |