Upload 3 files
Browse files- app.js +38 -7
- data.js +0 -0
- generate-data.mjs +64 -40
app.js
CHANGED
|
@@ -183,8 +183,20 @@ function latestRecordsBySample(records) {
|
|
| 183 |
return map;
|
| 184 |
}
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
function documentationRecord(row) {
|
| 187 |
-
return
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
function hydrateRecordKeys(records) {
|
|
@@ -211,11 +223,14 @@ function hydrateRecordKeys(records) {
|
|
| 211 |
function preserveDocumentedRows(records) {
|
| 212 |
if (!payload?.rows?.length || !Array.isArray(records) || !records.length) return;
|
| 213 |
const existingKeys = new Set(payload.rows.map((row) => row.sampleKey));
|
|
|
|
| 214 |
const extraRows = [];
|
| 215 |
|
| 216 |
records.forEach((record) => {
|
| 217 |
if (!record.sampleKey || existingKeys.has(record.sampleKey) || !record.establishmentName) return;
|
|
|
|
| 218 |
existingKeys.add(record.sampleKey);
|
|
|
|
| 219 |
extraRows.push({
|
| 220 |
researcher: record.researcher || "غير محدد",
|
| 221 |
establishmentName: record.establishmentName,
|
|
@@ -277,15 +292,20 @@ function dateOffset(dateValue, offset) {
|
|
| 277 |
function adminSnapshot() {
|
| 278 |
const records = [...latestRecordsBySample(adminRecords).values()];
|
| 279 |
const recordMap = new Map(records.map((record) => [record.sampleKey, record]));
|
|
|
|
| 280 |
const today = riyadhToday();
|
| 281 |
const yesterday = dateOffset(today, -1);
|
| 282 |
-
const documented = payload.rows.filter((row) =>
|
|
|
|
|
|
|
|
|
|
| 283 |
const todayRecords = records.filter((record) => record.documentedDate === today);
|
| 284 |
const yesterdayRecords = records.filter((record) => record.documentedDate === yesterday);
|
| 285 |
const activeToday = new Set(todayRecords.map((record) => record.researcher).filter(Boolean));
|
| 286 |
return {
|
| 287 |
records,
|
| 288 |
recordMap,
|
|
|
|
| 289 |
today,
|
| 290 |
yesterday,
|
| 291 |
documented,
|
|
@@ -857,7 +877,11 @@ function createStat(value, label, className) {
|
|
| 857 |
|
| 858 |
function renderSummary() {
|
| 859 |
const records = latestRecordsBySample(documentationRecords);
|
| 860 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 861 |
const total = researcherRows.length;
|
| 862 |
const remaining = Math.max(total - documented, 0);
|
| 863 |
const percentage = total ? Math.round((documented / total) * 100) : 0;
|
|
@@ -868,7 +892,7 @@ function renderSummary() {
|
|
| 868 |
day: "2-digit",
|
| 869 |
}).format(new Date());
|
| 870 |
const documentedToday = researcherRows.filter(
|
| 871 |
-
(row) => records.get(row.sampleKey)?.documentedDate === today,
|
| 872 |
).length;
|
| 873 |
const progress = document.createElement("div");
|
| 874 |
progress.className = "researcher-progress";
|
|
@@ -1479,19 +1503,22 @@ function completedDocumentationMap() {
|
|
| 1479 |
}
|
| 1480 |
|
| 1481 |
function isDocumentationCompleted(rowId) {
|
|
|
|
| 1482 |
return Boolean(
|
| 1483 |
latestRecordsBySample(documentationRecords).has(rowId) ||
|
|
|
|
| 1484 |
completedDocumentationMap()[rowId],
|
| 1485 |
);
|
| 1486 |
}
|
| 1487 |
|
| 1488 |
function documentedRowsFirst(rows) {
|
| 1489 |
const records = latestRecordsBySample(documentationRecords);
|
|
|
|
| 1490 |
const locallyCompleted = completedDocumentationMap();
|
| 1491 |
|
| 1492 |
return [...rows].sort((a, b) => {
|
| 1493 |
-
const aRecord = records.get(a.sampleKey);
|
| 1494 |
-
const bRecord = records.get(b.sampleKey);
|
| 1495 |
const aCompleted = Boolean(aRecord || locallyCompleted[a.rowId]);
|
| 1496 |
const bCompleted = Boolean(bRecord || locallyCompleted[b.rowId]);
|
| 1497 |
|
|
@@ -1923,7 +1950,11 @@ elements.documentationForm.addEventListener("submit", async (event) => {
|
|
| 1923 |
closeDocumentationDialog();
|
| 1924 |
applyFilters();
|
| 1925 |
const records = latestRecordsBySample(documentationRecords);
|
| 1926 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1927 |
const remainingCount = Math.max(researcherRows.length - completedCount, 0);
|
| 1928 |
showToast(
|
| 1929 |
remainingCount === 0
|
|
|
|
| 183 |
return map;
|
| 184 |
}
|
| 185 |
|
| 186 |
+
function latestRecordsByCommercialRecord(records) {
|
| 187 |
+
const map = new Map();
|
| 188 |
+
records.forEach((record) => {
|
| 189 |
+
if (!record.commercialRecord || map.has(record.commercialRecord)) return;
|
| 190 |
+
map.set(record.commercialRecord, record);
|
| 191 |
+
});
|
| 192 |
+
return map;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
function documentationRecord(row) {
|
| 196 |
+
return (
|
| 197 |
+
latestRecordsBySample(documentationRecords).get(row.sampleKey) ||
|
| 198 |
+
latestRecordsByCommercialRecord(documentationRecords).get(row.commercialRecord)
|
| 199 |
+
);
|
| 200 |
}
|
| 201 |
|
| 202 |
function hydrateRecordKeys(records) {
|
|
|
|
| 223 |
function preserveDocumentedRows(records) {
|
| 224 |
if (!payload?.rows?.length || !Array.isArray(records) || !records.length) return;
|
| 225 |
const existingKeys = new Set(payload.rows.map((row) => row.sampleKey));
|
| 226 |
+
const existingCommercialRecords = new Set(payload.rows.map((row) => row.commercialRecord).filter(Boolean));
|
| 227 |
const extraRows = [];
|
| 228 |
|
| 229 |
records.forEach((record) => {
|
| 230 |
if (!record.sampleKey || existingKeys.has(record.sampleKey) || !record.establishmentName) return;
|
| 231 |
+
if (record.commercialRecord && existingCommercialRecords.has(record.commercialRecord)) return;
|
| 232 |
existingKeys.add(record.sampleKey);
|
| 233 |
+
if (record.commercialRecord) existingCommercialRecords.add(record.commercialRecord);
|
| 234 |
extraRows.push({
|
| 235 |
researcher: record.researcher || "غير محدد",
|
| 236 |
establishmentName: record.establishmentName,
|
|
|
|
| 292 |
function adminSnapshot() {
|
| 293 |
const records = [...latestRecordsBySample(adminRecords).values()];
|
| 294 |
const recordMap = new Map(records.map((record) => [record.sampleKey, record]));
|
| 295 |
+
const commercialRecordMap = latestRecordsByCommercialRecord(records);
|
| 296 |
const today = riyadhToday();
|
| 297 |
const yesterday = dateOffset(today, -1);
|
| 298 |
+
const documented = payload.rows.filter((row) =>
|
| 299 |
+
recordMap.has(row.sampleKey) ||
|
| 300 |
+
Boolean(row.commercialRecord && commercialRecordMap.has(row.commercialRecord)),
|
| 301 |
+
);
|
| 302 |
const todayRecords = records.filter((record) => record.documentedDate === today);
|
| 303 |
const yesterdayRecords = records.filter((record) => record.documentedDate === yesterday);
|
| 304 |
const activeToday = new Set(todayRecords.map((record) => record.researcher).filter(Boolean));
|
| 305 |
return {
|
| 306 |
records,
|
| 307 |
recordMap,
|
| 308 |
+
commercialRecordMap,
|
| 309 |
today,
|
| 310 |
yesterday,
|
| 311 |
documented,
|
|
|
|
| 877 |
|
| 878 |
function renderSummary() {
|
| 879 |
const records = latestRecordsBySample(documentationRecords);
|
| 880 |
+
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
|
| 881 |
+
const documented = researcherRows.filter((row) =>
|
| 882 |
+
records.has(row.sampleKey) ||
|
| 883 |
+
Boolean(row.commercialRecord && commercialRecords.has(row.commercialRecord)),
|
| 884 |
+
).length;
|
| 885 |
const total = researcherRows.length;
|
| 886 |
const remaining = Math.max(total - documented, 0);
|
| 887 |
const percentage = total ? Math.round((documented / total) * 100) : 0;
|
|
|
|
| 892 |
day: "2-digit",
|
| 893 |
}).format(new Date());
|
| 894 |
const documentedToday = researcherRows.filter(
|
| 895 |
+
(row) => (records.get(row.sampleKey) || commercialRecords.get(row.commercialRecord))?.documentedDate === today,
|
| 896 |
).length;
|
| 897 |
const progress = document.createElement("div");
|
| 898 |
progress.className = "researcher-progress";
|
|
|
|
| 1503 |
}
|
| 1504 |
|
| 1505 |
function isDocumentationCompleted(rowId) {
|
| 1506 |
+
const row = researcherRows.find((item) => item.rowId === rowId || item.sampleKey === rowId);
|
| 1507 |
return Boolean(
|
| 1508 |
latestRecordsBySample(documentationRecords).has(rowId) ||
|
| 1509 |
+
(row?.commercialRecord && latestRecordsByCommercialRecord(documentationRecords).has(row.commercialRecord)) ||
|
| 1510 |
completedDocumentationMap()[rowId],
|
| 1511 |
);
|
| 1512 |
}
|
| 1513 |
|
| 1514 |
function documentedRowsFirst(rows) {
|
| 1515 |
const records = latestRecordsBySample(documentationRecords);
|
| 1516 |
+
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
|
| 1517 |
const locallyCompleted = completedDocumentationMap();
|
| 1518 |
|
| 1519 |
return [...rows].sort((a, b) => {
|
| 1520 |
+
const aRecord = records.get(a.sampleKey) || commercialRecords.get(a.commercialRecord);
|
| 1521 |
+
const bRecord = records.get(b.sampleKey) || commercialRecords.get(b.commercialRecord);
|
| 1522 |
const aCompleted = Boolean(aRecord || locallyCompleted[a.rowId]);
|
| 1523 |
const bCompleted = Boolean(bRecord || locallyCompleted[b.rowId]);
|
| 1524 |
|
|
|
|
| 1950 |
closeDocumentationDialog();
|
| 1951 |
applyFilters();
|
| 1952 |
const records = latestRecordsBySample(documentationRecords);
|
| 1953 |
+
const commercialRecords = latestRecordsByCommercialRecord(documentationRecords);
|
| 1954 |
+
const completedCount = researcherRows.filter((row) =>
|
| 1955 |
+
records.has(row.sampleKey) ||
|
| 1956 |
+
Boolean(row.commercialRecord && commercialRecords.has(row.commercialRecord)),
|
| 1957 |
+
).length;
|
| 1958 |
const remainingCount = Math.max(researcherRows.length - completedCount, 0);
|
| 1959 |
showToast(
|
| 1960 |
remainingCount === 0
|
data.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
generate-data.mjs
CHANGED
|
@@ -5,32 +5,32 @@ import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
|
| 5 |
const [inputPath, outputPath, password = "20302030", supervisorPassword = "1448"] = process.argv.slice(2);
|
| 6 |
|
| 7 |
const MAP_URLS = [
|
| 8 |
-
["
|
| 9 |
-
["
|
| 10 |
-
["
|
| 11 |
-
["
|
| 12 |
-
["
|
| 13 |
-
["
|
| 14 |
-
["
|
| 15 |
-
["
|
| 16 |
-
["
|
| 17 |
-
["
|
| 18 |
-
["
|
| 19 |
-
["
|
| 20 |
-
["
|
| 21 |
-
["
|
| 22 |
-
["
|
| 23 |
-
["
|
| 24 |
-
["
|
| 25 |
["لين أحمد بن عبدالعزيز القصير", "https://stat2025-map.static.hf.space/Rahn/18.html"],
|
| 26 |
-
["
|
| 27 |
-
["علي
|
| 28 |
-
["ن
|
| 29 |
-
["
|
| 30 |
-
["
|
| 31 |
-
["
|
| 32 |
-
["
|
| 33 |
-
["
|
| 34 |
];
|
| 35 |
|
| 36 |
function clean(value) {
|
|
@@ -87,6 +87,12 @@ function mapUrlFor(researcher) {
|
|
| 87 |
return partial?.[1] || "";
|
| 88 |
}
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
function sampleKey(row) {
|
| 91 |
const parts = [
|
| 92 |
row.commercialRecord,
|
|
@@ -128,18 +134,36 @@ if (!inputPath || !outputPath) {
|
|
| 128 |
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(inputPath));
|
| 129 |
const sheet = workbook.worksheets.getItemAt(0);
|
| 130 |
const values = sheet.getUsedRange(true).values;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
const rows = [];
|
| 132 |
|
| 133 |
for (const source of values.slice(1)) {
|
| 134 |
-
const researcher = clean(source[
|
| 135 |
-
const establishmentName = clean(source[
|
| 136 |
if (!researcher || !establishmentName) continue;
|
| 137 |
|
| 138 |
-
const madonStatement = clean(source[
|
| 139 |
-
const madonNote = clean(source[
|
| 140 |
const madonCoordinates = coordinateFromText(madonNote);
|
| 141 |
-
const x = numeric(source[
|
| 142 |
-
const y = numeric(source[
|
| 143 |
const baseCoordinates =
|
| 144 |
x !== null && y !== null && x >= 35 && x <= 60 && y >= 15 && y <= 35 ? `${y}, ${x}` : "";
|
| 145 |
const coordinates = madonCoordinates || baseCoordinates;
|
|
@@ -154,19 +178,19 @@ if (!inputPath || !outputPath) {
|
|
| 154 |
const row = {
|
| 155 |
researcher,
|
| 156 |
establishmentName,
|
| 157 |
-
contractNumber: clean(source[
|
| 158 |
-
city: canonicalCity(source[
|
| 159 |
-
sourceCity: clean(source[
|
| 160 |
-
representativeCity: canonicalCity("", source[
|
| 161 |
-
status: clean(source[
|
| 162 |
madonStatement,
|
| 163 |
madonNoteText: madonCoordinates ? "" : madonNote,
|
| 164 |
coordinates,
|
| 165 |
locationType,
|
| 166 |
-
commercialRecord: clean(source[
|
| 167 |
-
unifiedNumber: clean(source[
|
| 168 |
-
activityCode: clean(source[
|
| 169 |
-
activity: clean(source[
|
| 170 |
};
|
| 171 |
row.sampleKey = sampleKey(row);
|
| 172 |
rows.push(row);
|
|
|
|
| 5 |
const [inputPath, outputPath, password = "20302030", supervisorPassword = "1448"] = process.argv.slice(2);
|
| 6 |
|
| 7 |
const MAP_URLS = [
|
| 8 |
+
["أماني مصطفى عبدالله الطيب", "https://stat2025-map.static.hf.space/Rahn/01.html"],
|
| 9 |
+
["اسعد بن ماجد بن", "https://stat2025-map.static.hf.space/Rahn/02.html"],
|
| 10 |
+
["ريم بنت محمد بن عبدالعزيز الملحم", "https://stat2025-map.static.hf.space/Rahn/03.html"],
|
| 11 |
+
["زكي بن عيسى بن", "https://stat2025-map.static.hf.space/Rahn/04.html"],
|
| 12 |
+
["ساره حسين بن عبدالهادي بوخمسين", "https://stat2025-map.static.hf.space/Rahn/05.html"],
|
| 13 |
+
["ساره خالد سليمان المحيسن", "https://stat2025-map.static.hf.space/Rahn/06.html"],
|
| 14 |
+
["صالح عبدالله صالح الدخيل", "https://stat2025-map.static.hf.space/Rahn/07.html"],
|
| 15 |
+
["طيبه فالح بن عبدالله الرويشد", "https://stat2025-map.static.hf.space/Rahn/08.html"],
|
| 16 |
+
["عبدالرحمن عبدالله سعد الحماد", "https://stat2025-map.static.hf.space/Rahn/09.html"],
|
| 17 |
+
["عبدالعزيز فهد عبدالعزيز العبلان", "https://stat2025-map.static.hf.space/Rahn/10.html"],
|
| 18 |
+
["علي جابر بن علي", "https://stat2025-map.static.hf.space/Rahn/11.html"],
|
| 19 |
+
["عماد بن عيسى بن", "https://stat2025-map.static.hf.space/Rahn/12.html"],
|
| 20 |
+
["غادة سعد عبدالرحمن الراحله", "https://stat2025-map.static.hf.space/Rahn/13.html"],
|
| 21 |
+
["فارس سمير سليماني", "https://stat2025-map.static.hf.space/Rahn/14.html"],
|
| 22 |
+
["فاطمة حميدي هيف القحطاني", "https://stat2025-map.static.hf.space/Rahn/15.html"],
|
| 23 |
+
["فوز عائد نومان المطيري", "https://stat2025-map.static.hf.space/Rahn/16.html"],
|
| 24 |
+
["قنوت محمد بن عبدالله آل حماد", "https://stat2025-map.static.hf.space/Rahn/17.html"],
|
| 25 |
["لين أحمد بن عبدالعزيز القصير", "https://stat2025-map.static.hf.space/Rahn/18.html"],
|
| 26 |
+
["ماجد سعد ناصر السبيعي", "https://stat2025-map.static.hf.space/Rahn/19.html"],
|
| 27 |
+
["مرتضى عبدالجليل بن عيسى", "https://stat2025-map.static.hf.space/Rahn/20.html"],
|
| 28 |
+
["ناصر منصور علي الرويس", "https://stat2025-map.static.hf.space/Rahn/21.html"],
|
| 29 |
+
["نبأ عادل بن عبدالكريم", "https://stat2025-map.static.hf.space/Rahn/22.html"],
|
| 30 |
+
["نوار عوض مقبل العنزى", "https://stat2025-map.static.hf.space/Rahn/23.html"],
|
| 31 |
+
["نوف سعود بن سالم الخثعمي", "https://stat2025-map.static.hf.space/Rahn/24.html"],
|
| 32 |
+
["نيللي حسين عبدالله الجعص", "https://stat2025-map.static.hf.space/Rahn/25.html"],
|
| 33 |
+
["هيه عبدالعزيز المزيني", "https://stat2025-map.static.hf.space/Rahn/26.html"],
|
| 34 |
];
|
| 35 |
|
| 36 |
function clean(value) {
|
|
|
|
| 87 |
return partial?.[1] || "";
|
| 88 |
}
|
| 89 |
|
| 90 |
+
function columnIndex(headers, names, fallback) {
|
| 91 |
+
const targets = names.map(normalize);
|
| 92 |
+
const index = headers.findIndex((header) => targets.includes(normalize(header)));
|
| 93 |
+
return index >= 0 ? index : fallback;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
function sampleKey(row) {
|
| 97 |
const parts = [
|
| 98 |
row.commercialRecord,
|
|
|
|
| 134 |
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(inputPath));
|
| 135 |
const sheet = workbook.worksheets.getItemAt(0);
|
| 136 |
const values = sheet.getUsedRange(true).values;
|
| 137 |
+
const headers = (values[0] || []).map(clean);
|
| 138 |
+
const columns = {
|
| 139 |
+
commercialRecord: columnIndex(headers, ["السجل التجاري"], 11),
|
| 140 |
+
researcher: columnIndex(headers, ["اسم الباحث/ة", "اسم الباحث", "الباحث"], 0),
|
| 141 |
+
establishmentName: columnIndex(headers, ["إسم المنشأة", "اسم المنشأة", "المنشأة"], 1),
|
| 142 |
+
alternateName: 2,
|
| 143 |
+
contractNumber: columnIndex(headers, ["رقم العقد"], 3),
|
| 144 |
+
city: columnIndex(headers, ["المدينة الصناعية", "توضيح المدينة"], 5),
|
| 145 |
+
fallbackCity: 4,
|
| 146 |
+
status: columnIndex(headers, ["حالة الاستيفاء"], 6),
|
| 147 |
+
madonStatement: columnIndex(headers, ["افادة مدن", "إفادة مدن"], 7),
|
| 148 |
+
madonNote: columnIndex(headers, ["ملاحظة مدن", "ملاحظات مدن"], 8),
|
| 149 |
+
x: columnIndex(headers, ["اكس", "x"], 9),
|
| 150 |
+
y: columnIndex(headers, ["واي", "y"], 10),
|
| 151 |
+
unifiedNumber: columnIndex(headers, ["الرقم الموحد"], 12),
|
| 152 |
+
activityCode: columnIndex(headers, ["ترميز النشاط"], 13),
|
| 153 |
+
activity: columnIndex(headers, ["النشاط"], 14),
|
| 154 |
+
};
|
| 155 |
const rows = [];
|
| 156 |
|
| 157 |
for (const source of values.slice(1)) {
|
| 158 |
+
const researcher = clean(source[columns.researcher]);
|
| 159 |
+
const establishmentName = clean(source[columns.establishmentName]) || clean(source[columns.alternateName]);
|
| 160 |
if (!researcher || !establishmentName) continue;
|
| 161 |
|
| 162 |
+
const madonStatement = clean(source[columns.madonStatement]);
|
| 163 |
+
const madonNote = clean(source[columns.madonNote]);
|
| 164 |
const madonCoordinates = coordinateFromText(madonNote);
|
| 165 |
+
const x = numeric(source[columns.x]);
|
| 166 |
+
const y = numeric(source[columns.y]);
|
| 167 |
const baseCoordinates =
|
| 168 |
x !== null && y !== null && x >= 35 && x <= 60 && y >= 15 && y <= 35 ? `${y}, ${x}` : "";
|
| 169 |
const coordinates = madonCoordinates || baseCoordinates;
|
|
|
|
| 178 |
const row = {
|
| 179 |
researcher,
|
| 180 |
establishmentName,
|
| 181 |
+
contractNumber: clean(source[columns.contractNumber]),
|
| 182 |
+
city: canonicalCity(source[columns.city], source[columns.fallbackCity]),
|
| 183 |
+
sourceCity: clean(source[columns.city]) || clean(source[columns.fallbackCity]),
|
| 184 |
+
representativeCity: canonicalCity("", source[columns.fallbackCity] || source[columns.city]),
|
| 185 |
+
status: clean(source[columns.status]),
|
| 186 |
madonStatement,
|
| 187 |
madonNoteText: madonCoordinates ? "" : madonNote,
|
| 188 |
coordinates,
|
| 189 |
locationType,
|
| 190 |
+
commercialRecord: clean(source[columns.commercialRecord]),
|
| 191 |
+
unifiedNumber: clean(source[columns.unifiedNumber]),
|
| 192 |
+
activityCode: clean(source[columns.activityCode]),
|
| 193 |
+
activity: clean(source[columns.activity]),
|
| 194 |
};
|
| 195 |
row.sampleKey = sampleKey(row);
|
| 196 |
rows.push(row);
|