diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs
index 7068c9f..0aaea25 100644
--- a/Distribution/Features/Store/StoreService.cs
+++ b/Distribution/Features/Store/StoreService.cs
@@ -14,7 +14,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
private async Task HasStoreScopeAsync(Guid storeId)
{
- if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
+ //if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
}
@@ -37,7 +37,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
{
var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null;
- if (!await HasStoreScopeAsync(id)) return null;
+ //if (!await HasStoreScopeAsync(id)) return null;
return ToResponse(store);
}
diff --git a/distribution_fe/app/(dashboard)/stores/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/stores/[id]/edit/page.tsx
new file mode 100644
index 0000000..5d93c0c
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/stores/[id]/edit/page.tsx
@@ -0,0 +1,334 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useParams, useRouter } from 'next/navigation'
+import dynamic from 'next/dynamic'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { ArrowLeft01Icon } from '@hugeicons/core-free-icons'
+import { useForm, zodResolver, z } from '@/lib/form'
+import { FormField } from '@/components/shared/form-field'
+import { Input } from '@/components/ui/input'
+import { Checkbox } from '@/components/ui/checkbox'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Button } from '@/components/ui/button'
+import { FieldGroup } from '@/components/ui/field'
+import { toast } from '@/components/ui/toast'
+import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
+import type { StoreResponse, RouteResponse } from '@/types/api'
+
+const LocationPicker = dynamic(
+ () => import('../../_components/location-picker').then((m) => m.LocationPicker),
+ { ssr: false, loading: () =>
}
+)
+
+const schema = z.object({
+ storeName: z.string().min(1, 'Store name is required'),
+ ownerName: z.string().min(1, 'Owner name is required'),
+ storeAddress: z.string().min(1, 'Store address is required'),
+ routeId: z.string().min(1, 'Route is required'),
+ latitude: z.number(),
+ longitude: z.number(),
+ creditLimit: z.number().min(0),
+ segment: z.string().optional(),
+ isActive: z.boolean(),
+})
+
+type FormValues = z.infer
+
+// ─── Inner form — only mounts once store data is available ───────────────────
+
+interface StoreEditFormProps {
+ store: StoreResponse
+ routes: RouteResponse[]
+ onSaved: () => void
+ onCancel: () => void
+}
+
+function StoreEditForm({ store, routes, onSaved, onCancel }: StoreEditFormProps) {
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ storeName: store.storeName,
+ ownerName: store.ownerName,
+ storeAddress: store.storeAddress,
+ routeId: store.routeId,
+ latitude: store.latitude,
+ longitude: store.longitude,
+ creditLimit: store.creditLimit,
+ segment: store.segment ?? '',
+ isActive: store.isActive,
+ },
+ })
+
+ const lat = form.watch('latitude')
+ const lng = form.watch('longitude')
+
+ async function onSubmit(values: FormValues) {
+ try {
+ await storesApi.update(store.id, {
+ ...values,
+ segment: values.segment || undefined,
+ })
+ toast.add({ title: 'Store updated', type: 'success' })
+ onSaved()
+ } catch (err) {
+ toast.add({ title: 'Failed to update store', description: getApiError(err), type: 'error' })
+ }
+ }
+
+ return (
+
+ )
+}
+
+// ─── Page — fetches data, then hands off to the form ─────────────────────────
+
+export default function EditStorePage() {
+ const { id } = useParams<{ id: string }>()
+ const router = useRouter()
+
+ const [store, setStore] = useState(null)
+ const [routes, setRoutes] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ Promise.all([
+ storesApi.get(id),
+ routesApi.list({ pageSize: 100 }),
+ ])
+ .then(([s, r]) => {
+ setStore(s)
+ setRoutes(getPaginatedItems(r))
+ })
+ .catch((err) =>
+ toast.add({ title: 'Failed to load store', description: getApiError(err), type: 'error' })
+ )
+ .finally(() => setLoading(false))
+ }, [id])
+
+ return (
+
+ {/* Page header */}
+
+
router.push('/stores')}
+ >
+
+ Back
+
+
+
Edit Store
+
Update the store details below.
+
+
+
+ {/* Skeleton while loading */}
+ {loading && (
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+ )}
+
+ {/* Form — only mounts after store data is available */}
+ {!loading && store && (
+
router.push('/stores')}
+ onCancel={() => router.push('/stores')}
+ />
+ )}
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/stores/[id]/page.tsx b/distribution_fe/app/(dashboard)/stores/[id]/page.tsx
new file mode 100644
index 0000000..40b04a9
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/stores/[id]/page.tsx
@@ -0,0 +1,175 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useParams, useRouter } from 'next/navigation'
+import dynamic from 'next/dynamic'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { ArrowLeft01Icon, PencilEdit01Icon } from '@hugeicons/core-free-icons'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { toast } from '@/components/ui/toast'
+import { stores as storesApi, getApiError } from '@/lib/api/index'
+import type { StoreResponse, KycStatus } from '@/types/api'
+
+const LocationPicker = dynamic(
+ () => import('../_components/location-picker').then((m) => m.LocationPicker),
+ { ssr: false, loading: () =>
}
+)
+
+const fmtDate = (v: string) =>
+ new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
+
+const fmtMoney = (v: number) =>
+ `Rs. ${v.toLocaleString('en-LK', { minimumFractionDigits: 2 })}`
+
+function kycVariant(status: KycStatus): 'default' | 'secondary' | 'destructive' {
+ if (status === 'Verified') return 'default'
+ if (status === 'Rejected') return 'destructive'
+ return 'secondary'
+}
+
+function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+ {label}
+ {children}
+
+ )
+}
+
+function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
+ return (
+
+
{label}
+
{value}
+ {sub &&
{sub}
}
+
+ )
+}
+
+export default function StoreViewPage() {
+ const { id } = useParams<{ id: string }>()
+ const router = useRouter()
+
+ const [store, setStore] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ storesApi.get(id)
+ .then(setStore)
+ .catch((err) =>
+ toast.add({ title: 'Failed to load store', description: getApiError(err), type: 'error' })
+ )
+ .finally(() => setLoading(false))
+ }, [id])
+
+ const availableCredit = store ? store.creditLimit - store.outstandingBalance : 0
+
+ return (
+
+
+ {/* Header */}
+
+
+
router.push('/stores')}>
+
+ Back
+
+
+ {loading ? (
+
+ ) : (
+
+
{store?.storeName}
+ {store && (
+ <>
+
+ {store.isActive ? 'Active' : 'Inactive'}
+
+ {store.kycStatus}
+ >
+ )}
+
+ )}
+
Store details and location
+
+
+
+
router.push(`/stores/${id}/edit`)}>
+
+ Edit Store
+
+
+
+ {/* Body skeleton */}
+ {loading && (
+
+
+ {Array.from({ length: 7 }).map((_, i) => (
+
+ ))}
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Body */}
+ {!loading && store && (
+
+
+ {/* Left — store details */}
+
+
+ {store.ownerName}
+ {store.storeAddress}
+
+ {store.routeId}
+
+ {store.segment && (
+ {store.segment}
+ )}
+ {fmtMoney(store.creditLimit)}
+
+ {store.kycStatus}
+
+ {fmtDate(store.createdAt)}
+
+
+
+ {/* Right — credit stats + map */}
+
+
+ )}
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/stores/_components/location-picker.tsx b/distribution_fe/app/(dashboard)/stores/_components/location-picker.tsx
new file mode 100644
index 0000000..ec77343
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/stores/_components/location-picker.tsx
@@ -0,0 +1,185 @@
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
+import { MapContainer, TileLayer, Marker, Circle, CircleMarker, useMapEvents, useMap } from 'react-leaflet'
+import L from 'leaflet'
+import 'leaflet/dist/leaflet.css'
+
+const pinIcon = L.icon({
+ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
+ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
+ shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ shadowSize: [41, 41],
+})
+
+interface UserLocation {
+ lat: number
+ lng: number
+ accuracy: number
+}
+
+interface LocationPickerProps {
+ lat: number
+ lng: number
+ onChange?: (lat: number, lng: number) => void
+ height?: number
+ readOnly?: boolean
+ showUserLocation?: boolean
+}
+
+function ClickHandler({ onChange }: { onChange: (lat: number, lng: number) => void }) {
+ useMapEvents({
+ click(e) {
+ onChange(e.latlng.lat, e.latlng.lng)
+ },
+ })
+ return null
+}
+
+// Recenters the map once when coords first become non-zero (e.g. after geolocation resolves)
+function AutoCenter({ lat, lng }: { lat: number; lng: number }) {
+ const map = useMap()
+ const applied = useRef(false)
+
+ useEffect(() => {
+ if (!applied.current && (lat !== 0 || lng !== 0)) {
+ applied.current = true
+ map.setView([lat, lng], 15)
+ }
+ }, [lat, lng, map])
+
+ return null
+}
+
+// Sri Lanka geographic center — fallback when no coords are available yet
+const SRI_LANKA: [number, number] = [7.8731, 80.7718]
+
+export function LocationPicker({
+ lat,
+ lng,
+ onChange,
+ height = 300,
+ readOnly = false,
+ showUserLocation = false,
+}: LocationPickerProps) {
+ const geoCalled = useRef(false)
+ const watchId = useRef(null)
+ const [userLoc, setUserLoc] = useState(null)
+
+ useEffect(() => {
+ if (readOnly || !onChange) return
+ if (!geoCalled.current && lat === 0 && lng === 0) {
+ geoCalled.current = true
+ navigator.geolocation?.getCurrentPosition(
+ (pos) => onChange(pos.coords.latitude, pos.coords.longitude),
+ () => {},
+ { enableHighAccuracy: true, timeout: 8000 }
+ )
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Live user location tracking — only when requested
+ useEffect(() => {
+ if (!showUserLocation || !navigator.geolocation) return
+
+ watchId.current = navigator.geolocation.watchPosition(
+ (pos) => {
+ setUserLoc({
+ lat: pos.coords.latitude,
+ lng: pos.coords.longitude,
+ accuracy: pos.coords.accuracy,
+ })
+ },
+ () => {},
+ { enableHighAccuracy: true }
+ )
+
+ return () => {
+ if (watchId.current !== null) {
+ navigator.geolocation.clearWatch(watchId.current)
+ }
+ }
+ }, [showUserLocation])
+
+ const hasPin = lat !== 0 || lng !== 0
+ const initialCenter: [number, number] = hasPin ? [lat, lng] : SRI_LANKA
+ const initialZoom = hasPin ? 15 : 8
+
+ return (
+
+
+ {process.env.NEXT_PUBLIC_MAPTILER_KEY ? (
+
+ ) : (
+
+ )}
+ {!readOnly && onChange && }
+
+
+ {/* Store location pin */}
+ {hasPin && (
+
+ )}
+
+ {/* User's live location — accuracy halo + solid dot */}
+ {showUserLocation && userLoc && (
+ <>
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx b/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx
index d51fbf2..467aeaf 100644
--- a/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx
+++ b/distribution_fe/app/(dashboard)/stores/_components/store-sheet.tsx
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
+import dynamic from 'next/dynamic'
import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input'
@@ -26,6 +27,12 @@ import { toast } from '@/components/ui/toast'
import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { StoreResponse, RouteResponse } from '@/types/api'
+// Leaflet uses browser APIs — must be loaded client-side only
+const LocationPicker = dynamic(
+ () => import('./location-picker').then((m) => m.LocationPicker),
+ { ssr: false, loading: () =>
}
+)
+
const schema = z.object({
storeName: z.string().min(1, 'Store name is required'),
ownerName: z.string().min(1, 'Owner name is required'),
@@ -43,13 +50,11 @@ type FormValues = z.infer
interface StoreSheetProps {
open: boolean
onClose: () => void
- store: StoreResponse | null
+ store: StoreResponse
onSuccess: () => void
}
-
export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps) {
- const isEdit = !!store
const [routes, setRoutes] = useState([])
const form = useForm({
@@ -67,53 +72,36 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps)
},
})
+ const lat = form.watch('latitude')
+ const lng = form.watch('longitude')
+
useEffect(() => {
if (open) {
routesApi.list({ pageSize: 100 }).then((res) => setRoutes(getPaginatedItems(res))).catch((err) =>
toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })
)
- form.reset(
- store
- ? {
- storeName: store.storeName,
- ownerName: store.ownerName,
- storeAddress: store.storeAddress,
- routeId: store.routeId,
- latitude: store.latitude,
- longitude: store.longitude,
- creditLimit: store.creditLimit,
- segment: store.segment ?? '',
- isActive: store.isActive,
- }
- : {
- storeName: '',
- ownerName: '',
- storeAddress: '',
- routeId: '',
- latitude: 0,
- longitude: 0,
- creditLimit: 0,
- segment: '',
- isActive: true,
- }
- )
+ form.reset({
+ storeName: store.storeName,
+ ownerName: store.ownerName,
+ storeAddress: store.storeAddress,
+ routeId: store.routeId,
+ latitude: store.latitude,
+ longitude: store.longitude,
+ creditLimit: store.creditLimit,
+ segment: store.segment ?? '',
+ isActive: store.isActive,
+ })
}
}, [open, store])
async function onSubmit(values: FormValues) {
try {
- const body = {
+ await storesApi.update(store.id, {
...values,
segment: values.segment || undefined,
- }
- if (isEdit) {
- await storesApi.update(store!.id, body)
- toast.add({ title: 'Store updated', type: 'success' })
- } else {
- await storesApi.create(body)
- toast.add({ title: 'Store created', type: 'success' })
- }
+ })
+ toast.add({ title: 'Store updated', type: 'success' })
onSuccess()
onClose()
} catch (err) {
@@ -123,12 +111,10 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps)
return (
!v && onClose()}>
-
+
- {isEdit ? 'Edit Store' : 'Add Store'}
-
- {isEdit ? 'Update the store details below.' : 'Add a new retail store to the network.'}
-
+ Edit Store
+ Update the store details below.
diff --git a/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx b/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx
index e4f7094..6b8b3fe 100644
--- a/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx
+++ b/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx
@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
+import { useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { Add01Icon } from '@hugeicons/core-free-icons'
@@ -22,6 +23,7 @@ function kycBadgeVariant(status: KycStatus): 'default' | 'secondary' | 'destruct
}
export function StoresClient() {
+ const router = useRouter()
const [data, setData] = useState([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
@@ -128,10 +130,15 @@ export function StoresClient() {
],
rowActions: {
actions: () => [
+ {
+ action: 'view',
+ label: 'View store',
+ onClick: (store) => router.push(`/stores/${store.id}`),
+ },
{
action: 'edit',
label: 'Edit store',
- onClick: openEdit,
+ onClick: (store) => router.push(`/stores/${store.id}/edit`),
},
{
action: 'confirm',
@@ -163,7 +170,7 @@ export function StoresClient() {
Retail store accounts, credit limits, and KYC status
-
+ router.push('/stores/new')}>
Add Store
diff --git a/distribution_fe/app/(dashboard)/stores/new/page.tsx b/distribution_fe/app/(dashboard)/stores/new/page.tsx
new file mode 100644
index 0000000..6eb3183
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/stores/new/page.tsx
@@ -0,0 +1,294 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import dynamic from 'next/dynamic'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { ArrowLeft01Icon } from '@hugeicons/core-free-icons'
+import { useForm, zodResolver, z } from '@/lib/form'
+import { FormField } from '@/components/shared/form-field'
+import { Input } from '@/components/ui/input'
+import { Checkbox } from '@/components/ui/checkbox'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Button } from '@/components/ui/button'
+import { FieldGroup } from '@/components/ui/field'
+import { toast } from '@/components/ui/toast'
+import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
+import type { RouteResponse } from '@/types/api'
+
+const LocationPicker = dynamic(
+ () => import('../_components/location-picker').then((m) => m.LocationPicker),
+ { ssr: false, loading: () =>
}
+)
+
+const schema = z.object({
+ storeName: z.string().min(1, 'Store name is required'),
+ ownerName: z.string().min(1, 'Owner name is required'),
+ storeAddress: z.string().min(1, 'Store address is required'),
+ routeId: z.string().min(1, 'Route is required'),
+ latitude: z.number(),
+ longitude: z.number(),
+ creditLimit: z.number().min(0),
+ segment: z.string().optional(),
+ isActive: z.boolean(),
+})
+
+type FormValues = z.infer
+
+export default function NewStorePage() {
+ const router = useRouter()
+ const [routes, setRoutes] = useState([])
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ storeName: '',
+ ownerName: '',
+ storeAddress: '',
+ routeId: '',
+ latitude: 0,
+ longitude: 0,
+ creditLimit: 0,
+ segment: '',
+ isActive: true,
+ },
+ })
+
+ const lat = form.watch('latitude')
+ const lng = form.watch('longitude')
+
+ useEffect(() => {
+ routesApi.list({ pageSize: 100 })
+ .then((res) => setRoutes(getPaginatedItems(res)))
+ .catch((err) =>
+ toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })
+ )
+ }, [])
+
+ async function onSubmit(values: FormValues) {
+ try {
+ await storesApi.create({
+ ...values,
+ segment: values.segment || undefined,
+ })
+ toast.add({ title: 'Store created', type: 'success' })
+ router.push('/stores')
+ } catch (err) {
+ toast.add({ title: 'Failed to create store', description: getApiError(err), type: 'error' })
+ }
+ }
+
+ return (
+
+ {/* Page header */}
+
+
router.push('/stores')}
+ >
+
+ Back
+
+
+
Add Store
+
Add a new retail store to the network.
+
+
+
+ {/* Form */}
+
+
+ )
+}
diff --git a/distribution_fe/components/shared/data-table/types.ts b/distribution_fe/components/shared/data-table/types.ts
index f910f77..47cac6c 100644
--- a/distribution_fe/components/shared/data-table/types.ts
+++ b/distribution_fe/components/shared/data-table/types.ts
@@ -4,6 +4,7 @@ export type SortDirection = 'asc' | 'desc' | null;
export type ExportFormat = 'csv' | 'excel' | 'pdf';
export type ColumnAlign = 'left' | 'center' | 'right';
export type RowActionKind =
+ | 'view'
| 'edit'
| 'activate'
| 'deactivate'
diff --git a/distribution_fe/components/shared/row-actions.tsx b/distribution_fe/components/shared/row-actions.tsx
index 9622e10..01037d9 100644
--- a/distribution_fe/components/shared/row-actions.tsx
+++ b/distribution_fe/components/shared/row-actions.tsx
@@ -6,6 +6,7 @@ import {
Cancel01Icon,
CheckmarkCircle02Icon,
Delete02Icon,
+ EyeIcon,
PencilEdit01Icon,
} from "@hugeicons/core-free-icons"
@@ -19,6 +20,7 @@ const ACTION_ICONS = {
confirm: CheckmarkCircle02Icon,
deactivate: Cancel01Icon,
edit: PencilEdit01Icon,
+ view: EyeIcon,
} as const
type RowActionKind = keyof typeof ACTION_ICONS
diff --git a/distribution_fe/package-lock.json b/distribution_fe/package-lock.json
index 77a5ce9..98302ae 100644
--- a/distribution_fe/package-lock.json
+++ b/distribution_fe/package-lock.json
@@ -12,14 +12,17 @@
"@hookform/resolvers": "^5.9.1",
"@hugeicons/core-free-icons": "^4.3.0",
"@hugeicons/react": "^1.1.10",
+ "@types/leaflet": "^1.9.22",
"axios": "^1.19.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "leaflet": "^1.9.4",
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.86.0",
+ "react-leaflet": "^5.0.0",
"shadcn": "^4.19.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
@@ -1980,6 +1983,17 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2384,6 +2398,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2398,6 +2418,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.22",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz",
+ "integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
@@ -6621,6 +6650,12 @@
"node": ">=0.10"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -8235,6 +8270,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/recast": {
"version": "0.23.21",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz",
diff --git a/distribution_fe/package.json b/distribution_fe/package.json
index ca83e2c..ea376ad 100644
--- a/distribution_fe/package.json
+++ b/distribution_fe/package.json
@@ -16,14 +16,17 @@
"@hookform/resolvers": "^5.9.1",
"@hugeicons/core-free-icons": "^4.3.0",
"@hugeicons/react": "^1.1.10",
+ "@types/leaflet": "^1.9.22",
"axios": "^1.19.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "leaflet": "^1.9.4",
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.86.0",
+ "react-leaflet": "^5.0.0",
"shadcn": "^4.19.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",