diff --git a/frontend/components/settings/settings-location.tsx b/frontend/components/settings/settings-location.tsx index a9da765..94c7536 100644 --- a/frontend/components/settings/settings-location.tsx +++ b/frontend/components/settings/settings-location.tsx @@ -14,6 +14,7 @@ import { } from "@/components/settings/settings-parts"; import { cn } from "@/lib/utils"; import { getClinicSettings, saveClinicLocation } from "@/lib/clinic"; +import { reverseGeocode } from "@/lib/geocode"; import { notify } from "@/lib/toast"; // Parse a coordinate input into a number or null (empty ⇒ null). Returns @@ -29,7 +30,7 @@ function parseCoord(value: string): number | null | false { // Persists the clinic's address + optional map coordinates so the wallet app can // display the clinic location later. export function ClinicLocationSection() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [address, setAddress] = useState(""); const [city, setCity] = useState(""); const [country, setCountry] = useState(""); @@ -62,8 +63,10 @@ export function ClinicLocationSection() { }; }, []); - // Fill the coordinates from the browser's geolocation (the clinician runs this - // on a device at the clinic). Client-only — no backend or map service. + // Fill the location from the browser's geolocation (the clinician runs this on + // a device at the clinic): sets the coordinates, then reverse-geocodes them to + // also fill address / city / country. The address lookup is best-effort — on + // failure we keep the coordinates and just tell the user to fill the rest. const useMyLocation = () => { if (typeof navigator === "undefined" || !("geolocation" in navigator)) { notify.error( @@ -74,10 +77,23 @@ export function ClinicLocationSection() { } setLocating(true); navigator.geolocation.getCurrentPosition( - (pos) => { - setLatitude(pos.coords.latitude.toFixed(6)); - setLongitude(pos.coords.longitude.toFixed(6)); + async (pos) => { + const lat = pos.coords.latitude; + const lng = pos.coords.longitude; + setLatitude(lat.toFixed(6)); + setLongitude(lng.toFixed(6)); + const place = await reverseGeocode(lat, lng, i18n.language); setLocating(false); + if (place) { + if (place.address) setAddress(place.address); + if (place.city) setCity(place.city); + if (place.country) setCountry(place.country); + } else { + notify.info( + t("settings.location.savedTitle"), + t("settings.location.geoPartial"), + ); + } }, () => { setLocating(false); diff --git a/frontend/components/sidebar-02/app-sidebar.tsx b/frontend/components/sidebar-02/app-sidebar.tsx index 5a7b904..2a4babc 100644 --- a/frontend/components/sidebar-02/app-sidebar.tsx +++ b/frontend/components/sidebar-02/app-sidebar.tsx @@ -31,7 +31,8 @@ export function DashboardSidebar() { const { t, i18n } = useTranslation(); // Anchor the sidebar to the right for RTL locales (Arabic) so the whole shell // mirrors instead of leaving the fixed sidebar pinned physically left. - const side = dirFor(i18n.language) === "rtl" ? "right" : "left"; + const isRtl = dirFor(i18n.language) === "rtl"; + const side = isRtl ? "right" : "left"; const role = useActiveRole(); const { allowed: aiAllowed } = useAiAccess(); const isCollapsed = state === "collapsed"; @@ -64,7 +65,11 @@ export function DashboardSidebar() { "flex md:pt-3.5", isCollapsed ? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-start md:justify-start" - : "flex-row items-center justify-between" + : "flex-row items-center justify-between", + // RTL (Arabic): stack the logo and the toggle/bell controls into a + // right-aligned column so the collapse arrow sits ABOVE the nav icons + // instead of being pinned to the opposite (left) edge. + isRtl && !isCollapsed && "flex-col items-end justify-start gap-y-3" )} > diff --git a/frontend/components/sidebar-02/nav-notifications.tsx b/frontend/components/sidebar-02/nav-notifications.tsx index 8fd8f51..b0bde89 100644 --- a/frontend/components/sidebar-02/nav-notifications.tsx +++ b/frontend/components/sidebar-02/nav-notifications.tsx @@ -15,6 +15,7 @@ import { MenuSeparator, MenuTrigger, } from "@/components/ui/menu"; +import { dirFor } from "@/lib/i18n/config"; import { markNotificationRead, notificationHref } from "@/lib/notifications"; import { useNotifications } from "@/lib/use-notifications"; @@ -30,9 +31,11 @@ function relativeTime(iso: string): string { } export function NotificationsPopover() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const router = useRouter(); const { items, unread, markAllRead } = useNotifications(); + // Open toward the content side; flips to the left under RTL. + const popupSide = dirFor(i18n.language) === "rtl" ? "left" : "right"; return ( )} - + {t("nav.notifications")} diff --git a/frontend/components/ui/sidebar.tsx b/frontend/components/ui/sidebar.tsx index 28789fa..4422b25 100644 --- a/frontend/components/ui/sidebar.tsx +++ b/frontend/components/ui/sidebar.tsx @@ -299,7 +299,8 @@ export function SidebarTrigger({ variant="ghost" {...props} > - + {/* Mirror the panel arrow under RTL so it points toward the sidebar edge. */} + Toggle Sidebar ); diff --git a/frontend/lib/geocode.ts b/frontend/lib/geocode.ts new file mode 100644 index 0000000..098167e --- /dev/null +++ b/frontend/lib/geocode.ts @@ -0,0 +1,64 @@ +// Reverse geocoding via OpenStreetMap Nominatim. Used by the clinic-location +// settings so "Use my current location" can fill the address/city/country +// fields, not just the raw coordinates. +// +// Nominatim's usage policy asks for a descriptive User-Agent/Referer and low +// request volumes — this is only hit on an explicit button click, so occasional +// lookups are well within the acceptable-use limits. + +export interface ReverseGeocodeResult { + address: string; + city: string; + country: string; +} + +// Reverse-geocode a coordinate into a best-effort street address, city, and +// country. Resolves to `null` on any failure (network, rate limit, no match) so +// the caller can silently fall back to coordinates-only. +export async function reverseGeocode( + latitude: number, + longitude: number, + language?: string, +): Promise { + try { + const url = new URL("https://nominatim.openstreetmap.org/reverse"); + url.searchParams.set("format", "jsonv2"); + url.searchParams.set("lat", String(latitude)); + url.searchParams.set("lon", String(longitude)); + url.searchParams.set("zoom", "18"); + url.searchParams.set("addressdetails", "1"); + + const res = await fetch(url.toString(), { + headers: { + // Nominatim requires an identifying header; browsers forbid setting + // User-Agent, so Accept-Language localises the returned names. + "Accept-Language": language || "en", + }, + }); + if (!res.ok) return null; + const data = (await res.json()) as { + address?: Record; + display_name?: string; + }; + const a = data.address ?? {}; + + // Compose a street line from the most specific parts available. + const street = [a.house_number, a.road].filter(Boolean).join(" ").trim(); + const address = + street || + a.neighbourhood || + a.suburb || + a.pedestrian || + (data.display_name ? data.display_name.split(",")[0] : "") || + ""; + + const city = + a.city || a.town || a.village || a.municipality || a.county || a.state || ""; + const country = a.country || ""; + + if (!address && !city && !country) return null; + return { address, city, country }; + } catch { + return null; + } +} diff --git a/frontend/lib/i18n/locales/ar/translation.json b/frontend/lib/i18n/locales/ar/translation.json index 3aa53a0..d48e64c 100644 --- a/frontend/lib/i18n/locales/ar/translation.json +++ b/frontend/lib/i18n/locales/ar/translation.json @@ -2037,7 +2037,8 @@ "useMyLocation": "استخدام موقعي الحالي", "locating": "جارٍ تحديد الموقع…", "geoUnsupported": "الموقع غير متاح على هذا الجهاز.", - "geoError": "تعذّر الحصول على موقعك. تحقّق من الأذونات وحاول مرة أخرى." + "geoError": "تعذّر الحصول على موقعك. تحقّق من الأذونات وحاول مرة أخرى.", + "geoPartial": "تم إدخال الإحداثيات — الرجاء إضافة تفاصيل العنوان يدويًا." }, "signing": { "keyTitle": "مفتاح التوقيع", diff --git a/frontend/lib/i18n/locales/de/translation.json b/frontend/lib/i18n/locales/de/translation.json index 6dfa832..f860e76 100644 --- a/frontend/lib/i18n/locales/de/translation.json +++ b/frontend/lib/i18n/locales/de/translation.json @@ -2017,7 +2017,8 @@ "useMyLocation": "Aktuellen Standort verwenden", "locating": "Standort wird ermittelt …", "geoUnsupported": "Standort ist auf diesem Gerät nicht verfügbar.", - "geoError": "Standort konnte nicht ermittelt werden. Bitte Berechtigungen prüfen und erneut versuchen." + "geoError": "Standort konnte nicht ermittelt werden. Bitte Berechtigungen prüfen und erneut versuchen.", + "geoPartial": "Koordinaten eingetragen – bitte Adressdetails manuell ergänzen." }, "signing": { "keyTitle": "Signierschlüssel", diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index 9ea1493..27b10d4 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -2017,7 +2017,8 @@ "useMyLocation": "Use my current location", "locating": "Locating…", "geoUnsupported": "Location isn't available on this device.", - "geoError": "Couldn't get your location. Check permissions and try again." + "geoError": "Couldn't get your location. Check permissions and try again.", + "geoPartial": "Filled the coordinates — please add the address details manually." }, "signing": { "keyTitle": "Signing key", diff --git a/frontend/lib/i18n/locales/fr/translation.json b/frontend/lib/i18n/locales/fr/translation.json index f273b40..21b32a0 100644 --- a/frontend/lib/i18n/locales/fr/translation.json +++ b/frontend/lib/i18n/locales/fr/translation.json @@ -2017,7 +2017,8 @@ "useMyLocation": "Utiliser ma position actuelle", "locating": "Localisation…", "geoUnsupported": "La localisation n'est pas disponible sur cet appareil.", - "geoError": "Impossible d'obtenir votre position. Vérifiez les autorisations et réessayez." + "geoError": "Impossible d'obtenir votre position. Vérifiez les autorisations et réessayez.", + "geoPartial": "Coordonnées renseignées — veuillez ajouter l'adresse manuellement." }, "signing": { "keyTitle": "Clé de signature", diff --git a/frontend/lib/i18n/locales/so/translation.json b/frontend/lib/i18n/locales/so/translation.json index 7c8943a..1aba66e 100644 --- a/frontend/lib/i18n/locales/so/translation.json +++ b/frontend/lib/i18n/locales/so/translation.json @@ -2017,7 +2017,8 @@ "useMyLocation": "Isticmaal goobtayda hadda", "locating": "Waa la helayaa goobta…", "geoUnsupported": "Goobta laguma heli karo qalabkan.", - "geoError": "Lama heli karin goobtaada. Hubi oggolaanshaha oo isku day mar kale." + "geoError": "Lama heli karin goobtaada. Hubi oggolaanshaha oo isku day mar kale.", + "geoPartial": "Isudhigyada waa la buuxiyay — fadlan gacanta ku gali faahfaahinta ciwaanka." }, "signing": { "keyTitle": "Furaha saxiixa",