More changes than expected. Mobile version improvements & PWA
Build & Push Docker Images / test-backend (push) Successful in 44s
Build & Push Docker Images / test-frontend (push) Successful in 52s
Build & Push Docker Images / build-backend (push) Failing after 34s
Build & Push Docker Images / build-frontend (push) Failing after 30s

This commit is contained in:
Ichitux
2026-05-20 22:28:17 +02:00
parent 7b288d33cb
commit 2eff93e66a
118 changed files with 13780 additions and 209 deletions
+5
View File
@@ -2,6 +2,8 @@
* Shared helpers: OSM tags → single-line address, element → pharmacy row.
*/
import { parseOsmOpeningHours } from './opening-hours-osm.js';
export function buildAddressFromOsmTags(tags) {
if (!tags || typeof tags !== 'object') return '';
if (tags['addr:full'] && String(tags['addr:full']).trim()) {
@@ -47,11 +49,14 @@ export function osmElementToPharmacy(el) {
const phone =
tags.phone || tags['contact:phone'] || tags['contact:mobile'] || tags['contact:whatsapp'] || null;
const opening_hours = parseOsmOpeningHours(tags.opening_hours);
return {
name,
address: address || null,
phone: phone ? String(phone).trim() : null,
latitude: Number.isFinite(lat) ? lat : null,
longitude: Number.isFinite(lon) ? lon : null,
opening_hours: opening_hours || null,
};
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Parse a subset of the OSM opening_hours format into FarmaFinder's internal
* shape: { mon: ["09:00", "21:00"], ..., sun: null }.
*
* Supports the patterns commonly seen on amenity=pharmacy nodes:
* "24/7"
* "Mo-Fr 09:00-21:00"
* "Mo-Fr 09:00-21:00; Sa 09:00-14:00; Su closed"
* "Mo,We,Fr 09:00-14:00"
* "Mo-Fr 09:00-13:30,16:30-20:00" (split shifts: opens first, closes last)
*
* Public-holiday rules (PH/SH) and weeknumber/date filters are ignored.
* Returns null if no usable rule was found.
*
* @param {string} input
* @returns {{mon: string[]|null, tue: string[]|null, wed: string[]|null, thu: string[]|null, fri: string[]|null, sat: string[]|null, sun: string[]|null}|null}
*/
export function parseOsmOpeningHours(input) {
if (!input || typeof input !== 'string') return null;
const cleaned = input.trim().replace(/\s+/g, ' ');
if (!cleaned) return null;
const result = { mon: null, tue: null, wed: null, thu: null, fri: null, sat: null, sun: null };
if (/^24\s*\/\s*7$/i.test(cleaned)) {
for (const k of Object.keys(result)) result[k] = ['00:00', '24:00'];
return result;
}
const rules = cleaned.split(';').map(r => r.trim()).filter(Boolean);
let anySet = false;
for (const rule of rules) {
const ruleNoComment = rule.replace(/\([^)]*\)/g, '').trim();
if (!ruleNoComment) continue;
const lower = ruleNoComment.toLowerCase();
if (lower === 'closed' || lower === 'off') continue;
const tokens = ruleNoComment.split(/\s+/);
const dayPart = tokens.shift();
const timePart = tokens.join(' ').trim();
if (!dayPart) continue;
const days = expandDayPart(dayPart);
if (!days.length) continue;
if (!timePart || /^(off|closed)$/i.test(timePart)) {
for (const d of days) result[d] = null;
anySet = true;
continue;
}
const range = collapseTimeRanges(timePart);
if (!range) continue;
for (const d of days) result[d] = range;
anySet = true;
}
return anySet ? result : null;
}
const DAY_CODE_TO_KEY = { mo: 'mon', tu: 'tue', we: 'wed', th: 'thu', fr: 'fri', sa: 'sat', su: 'sun' };
const DAY_ORDER = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
function expandDayPart(dayPart) {
const out = new Set();
for (const segment of dayPart.split(',')) {
const seg = segment.trim();
if (!seg) continue;
if (/^(ph|sh)(-(ph|sh))?$/i.test(seg)) continue;
const rangeMatch = seg.match(/^([A-Za-z]{2})-([A-Za-z]{2})$/);
if (rangeMatch) {
const a = DAY_CODE_TO_KEY[rangeMatch[1].toLowerCase()];
const b = DAY_CODE_TO_KEY[rangeMatch[2].toLowerCase()];
if (!a || !b) continue;
const ai = DAY_ORDER.indexOf(a);
const bi = DAY_ORDER.indexOf(b);
if (ai === -1 || bi === -1) continue;
if (ai <= bi) {
for (let i = ai; i <= bi; i++) out.add(DAY_ORDER[i]);
} else {
for (let i = ai; i < DAY_ORDER.length; i++) out.add(DAY_ORDER[i]);
for (let i = 0; i <= bi; i++) out.add(DAY_ORDER[i]);
}
continue;
}
const single = DAY_CODE_TO_KEY[seg.toLowerCase()];
if (single) out.add(single);
}
return [...out];
}
function collapseTimeRanges(timePart) {
const matches = [...timePart.matchAll(/(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})/g)];
if (matches.length === 0) return null;
const first = matches[0];
const last = matches[matches.length - 1];
const open = `${first[1].padStart(2, '0')}:${first[2]}`;
const close = `${last[3].padStart(2, '0')}:${last[4]}`;
return [open, close];
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "farma-finder-api-sources",
"private": true,
"type": "module"
}