stat2025 commited on
Commit
96dd2e4
·
verified ·
1 Parent(s): 300695f

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.js +38 -7
  2. data.js +0 -0
  3. 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 latestRecordsBySample(documentationRecords).get(row.sampleKey);
 
 
 
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) => recordMap.has(row.sampleKey));
 
 
 
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 documented = researcherRows.filter((row) => records.has(row.sampleKey)).length;
 
 
 
 
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 completedCount = researcherRows.filter((row) => records.has(row.sampleKey)).length;
 
 
 
 
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
- ["صالح عبدالله صالح الدخيل", "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,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[0]);
135
- const establishmentName = clean(source[1]) || clean(source[2]);
136
  if (!researcher || !establishmentName) continue;
137
 
138
- const madonStatement = clean(source[7]);
139
- const madonNote = clean(source[8]);
140
  const madonCoordinates = coordinateFromText(madonNote);
141
- const x = numeric(source[9]);
142
- const y = numeric(source[10]);
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[3]),
158
- city: canonicalCity(source[5], source[4]),
159
- sourceCity: clean(source[5]) || clean(source[4]),
160
- representativeCity: canonicalCity("", source[4]),
161
- status: clean(source[6]),
162
  madonStatement,
163
  madonNoteText: madonCoordinates ? "" : madonNote,
164
  coordinates,
165
  locationType,
166
- commercialRecord: clean(source[11]),
167
- unifiedNumber: clean(source[12]),
168
- activityCode: clean(source[13]),
169
- activity: clean(source[14]),
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);