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 ( +
+
+ + {/* Left — text fields */} +
+ + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + field.onChange(parseFloat(e.target.value) || 0)} + onBlur={field.onBlur} + placeholder="0" + aria-invalid={invalid} + /> + )} + /> + + ( + + )} + /> + + ( +
+ + +
+ )} + /> + +
+ + +
+
+
+ + {/* Right — map */} +
+
+

+ Store Location * +

+

+ Click the map or drag the pin to update the location. +

+
+ + { + form.setValue('latitude', newLat, { shouldValidate: true }) + form.setValue('longitude', newLng, { shouldValidate: true }) + }} + /> + + {(lat !== 0 || lng !== 0) && ( +

+ {lat.toFixed(6)}, {lng.toFixed(6)} +

+ )} +
+
+
+ ) +} + +// ─── 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 */} +
+ +
+

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 */} +
+
+ +
+ {loading ? ( +
+ ) : ( +
+

{store?.storeName}

+ {store && ( + <> + + {store.isActive ? 'Active' : 'Inactive'} + + {store.kycStatus} + + )} +
+ )} +

Store details and location

+
+
+ + +
+ + {/* 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.
@@ -211,43 +197,32 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps) )} /> - ( - field.onChange(parseFloat(e.target.value) || 0)} - onBlur={field.onBlur} - placeholder="e.g. 6.9271" - aria-invalid={invalid} - /> - )} - /> + {/* Location picker — replaces manual lat/lng inputs */} +
+
+

+ Store Location * +

+

+ Click the map or drag the pin to update the location. +

+
- ( - field.onChange(parseFloat(e.target.value) || 0)} - onBlur={field.onBlur} - placeholder="e.g. 79.8612" - aria-invalid={invalid} - /> + { + form.setValue('latitude', newLat, { shouldValidate: true }) + form.setValue('longitude', newLng, { shouldValidate: true }) + }} + /> + + {(lat !== 0 || lng !== 0) && ( +

+ {lat.toFixed(6)}, {lng.toFixed(6)} +

)} - /> +
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

- 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 */} +
+ +
+

Add Store

+

Add a new retail store to the network.

+
+
+ + {/* Form */} +
+
+ + {/* Left — text fields */} +
+ + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + field.onChange(parseFloat(e.target.value) || 0)} + onBlur={field.onBlur} + placeholder="0" + aria-invalid={invalid} + /> + )} + /> + + ( + + )} + /> + + ( +
+ + +
+ )} + /> + +
+ + +
+
+
+ + {/* Right — map */} +
+
+

+ Store Location * +

+

+ Your current location is used as the starting point. Click the map or drag the pin to adjust. +

+
+ +
+ { + form.setValue('latitude', newLat, { shouldValidate: true }) + form.setValue('longitude', newLng, { shouldValidate: true }) + }} + /> +
+ + {(lat !== 0 || lng !== 0) && ( +

+ {lat.toFixed(6)}, {lng.toFixed(6)} +

+ )} +
+
+
+
+ ) +} 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",