diff --git a/netmanager-app/.env.example b/netmanager-app/.env.example index e6926a321f..e0d6557748 100644 --- a/netmanager-app/.env.example +++ b/netmanager-app/.env.example @@ -1,3 +1,3 @@ NEXT_PUBLIC_API_TOKEN= -NEXT_PUBLIC_API_URL= +NEXT_PUBLIC_API_URL=https://staging-analytics.airqo.net/api/v2/ NEXT_PUBLIC_ENV=development \ No newline at end of file diff --git a/netmanager-app/app/(authenticated)/cohorts/[id]/page.tsx b/netmanager-app/app/(authenticated)/cohorts/[id]/page.tsx new file mode 100644 index 0000000000..d042803c7f --- /dev/null +++ b/netmanager-app/app/(authenticated)/cohorts/[id]/page.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { ChevronLeft, Copy, Search, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { AddDevicesDialog } from "@/components/cohorts/assign-cohort-devices"; + +// Sample cohort data +const cohortData = { + name: "victoria_sugar", + id: "675bd462c06188001333d4d5", + visibility: "true", +}; + +// Sample devices data +const devices = [ + { + name: "Aq_29", + description: "AIRQO UNIT with PMS5003 Victoria S", + site: "N/A", + deploymentStatus: "Deployed", + dateCreated: "2019-03-02T00:00:00.000Z", + }, + { + name: "Aq_34", + description: "AIRQO UNIT with PMS5003 Victoria S", + site: "N/A", + deploymentStatus: "Deployed", + dateCreated: "2019-03-14T00:00:00.000Z", + }, + { + name: "Aq_35", + description: "AIRQO UNIT with PMS5003 Victoria S", + site: "N/A", + deploymentStatus: "Deployed", + dateCreated: "2019-03-28T00:00:00.000Z", + }, +]; + +export default function CohortDetailsPage() { + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); + const [cohortDetails, setCohortDetails] = useState(cohortData); + + const handleCopyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const handleReset = () => { + setCohortDetails(cohortData); + }; + + const handleSave = () => { + console.log("Saving changes:", cohortDetails); + }; + + const filteredDevices = devices.filter((device) => + Object.values(device).some((value) => + String(value).toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + }; + + return ( +
+
+ + +
+ +
+
+
+ + + setCohortDetails({ ...cohortDetails, name: e.target.value }) + } + /> +
+
+ +
+ + +
+
+
+ +
+ + +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+ +
+

Cohort devices

+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ + + + Device Name + Description + Site + Deployment status + Date created + Actions + + + + {filteredDevices.map((device) => ( + + {device.name} + {device.description} + {device.site} + + + {device.deploymentStatus} + + + {formatDate(device.dateCreated)} + + + + + ))} + +
+
+
+
+
+ ); +} diff --git a/netmanager-app/app/(authenticated)/cohorts/page.tsx b/netmanager-app/app/(authenticated)/cohorts/page.tsx new file mode 100644 index 0000000000..e621bcd73a --- /dev/null +++ b/netmanager-app/app/(authenticated)/cohorts/page.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState } from "react"; +import { Search } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { useRouter } from "next/navigation"; +import { CreateCohortDialog } from "@/components/cohorts/create-cohort"; + +// Sample data +const cohorts = [ + { + name: "victoria_sugar", + numberOfDevices: 5, + visibility: true, + dateCreated: "2024-12-13T06:29:54.490Z", + }, + { + name: "nairobi_mobile", + numberOfDevices: 4, + visibility: false, + dateCreated: "2024-10-27T18:10:41.672Z", + }, + { + name: "car_free_day_demo", + numberOfDevices: 3, + visibility: true, + dateCreated: "2024-09-07T07:00:00.956Z", + }, + { + name: "nimr", + numberOfDevices: 4, + visibility: false, + dateCreated: "2024-01-31T05:32:52.642Z", + }, + { + name: "map", + numberOfDevices: 10, + visibility: true, + dateCreated: "2024-01-23T09:42:50.735Z", + }, +]; + +export default function CohortsPage() { + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); + + const filteredCohorts = cohorts.filter((cohort) => + cohort.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + }; + + return ( +
+
+
+

Cohort Registry

+

+ Manage and organize your device cohorts +

+
+ +
+ +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ + + + Cohort Name + Number of devices + Visibility + Date created + + + + {filteredCohorts.map((cohort) => ( + router.push(`/cohorts/${cohort.name}`)} + > + {cohort.name} + {cohort.numberOfDevices} + + + {cohort.visibility ? "Visible" : "Hidden"} + + + {formatDate(cohort.dateCreated)} + + ))} + +
+
+
+ ); +} diff --git a/netmanager-app/app/(authenticated)/devices/overview/page.tsx b/netmanager-app/app/(authenticated)/devices/overview/page.tsx index c9558856f3..ae14c255b1 100644 --- a/netmanager-app/app/(authenticated)/devices/overview/page.tsx +++ b/netmanager-app/app/(authenticated)/devices/overview/page.tsx @@ -93,7 +93,7 @@ export default function DevicesPage() { device._id?.toLowerCase().includes(searchLower) || device.description?.toLowerCase().includes(searchLower) || device.device_codes.some((code) => - code.toLowerCase().includes(searchLower) + code ? code.toLowerCase().includes(searchLower) : false ) ); }); diff --git a/netmanager-app/app/(authenticated)/grids/[id]/page.tsx b/netmanager-app/app/(authenticated)/grids/[id]/page.tsx new file mode 100644 index 0000000000..daa175459f --- /dev/null +++ b/netmanager-app/app/(authenticated)/grids/[id]/page.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { ChevronLeft, Copy, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { GridSitesTable } from "@/components/grids/grid-sites"; +import { DateRangePicker } from "@/components/grids/date-range-picker"; +import { useGridDetails, useUpdateGridDetails } from "@/core/hooks/useGrids"; +import { Grid } from "@/app/types/grids"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { toast } from "sonner"; + +export default function GridDetailsPage() { + const router = useRouter(); + const { id } = useParams(); + const { gridDetails, isLoading, error } = useGridDetails(id.toString()); + const { + updateGridDetails, + isLoading: isSaving, + error: saveError, + } = useUpdateGridDetails(id.toString()); + const [gridData, setGridData] = useState({ + name: "", + _id: "", + visibility: false, + admin_level: "", + network: "", + long_name: "", + createdAt: "", + sites: [], + numberOfSites: 0, + } as Grid); + const [originalGridData, setOriginalGridData] = useState({ + name: "", + _id: "", + visibility: false, + admin_level: "", + network: "", + long_name: "", + createdAt: "", + sites: [], + numberOfSites: 0, + } as Grid); + + // Memoize gridDetails to prevent unnecessary re-renders + const memoizedGridDetails = useMemo(() => gridDetails, [gridDetails]); + + useEffect(() => { + if (memoizedGridDetails) { + setGridData({ ...memoizedGridDetails }); + setOriginalGridData({ ...memoizedGridDetails }); + } + }, [memoizedGridDetails]); + + const handleCopyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const handleReset = () => { + setGridData({ ...originalGridData }); + }; + + const handleSave = async () => { + const updatedFields: Partial = { + name: gridData.name, + admin_level: gridData.admin_level, + }; + if (gridData.visibility !== originalGridData.visibility) { + updatedFields.visibility = gridData.visibility; + } + + try { + await updateGridDetails(updatedFields); + toast("Grid details updated successfully"); + } catch (error) { + console.error(error); + toast("Failed to update grid details"); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || saveError) { + return ( +
+ + + Error + + {error?.message || saveError?.message} + + +
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+ + {/* Grid Details Form */} +
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + {/* API Endpoints */} +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Grid Sites */} + + + Grid Sites details + + + + + + + {/* Reports Section */} +
+ {/* Data Summary Report */} + + + Generate Grid Data Summary Report + + +

+ Select the time period of your interest to generate the report + for this airloud +

+ + +
+

+ Grid data summary report will appear here +

+
+
+
+ + {/* Uptime Report */} + + + Generate Grid Uptime Report + + +

+ Select the time period of your interest to view the uptime + report for this airloud +

+ + +
+

+ Grid uptime report will appear here +

+
+
+
+
+
+
+ ); +} diff --git a/netmanager-app/app/(authenticated)/grids/page.tsx b/netmanager-app/app/(authenticated)/grids/page.tsx new file mode 100644 index 0000000000..a71d5b8e3a --- /dev/null +++ b/netmanager-app/app/(authenticated)/grids/page.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useState } from "react"; +import { Search, Download, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { CreateGridForm } from "@/components/grids/create-grid"; +import { useRouter } from "next/navigation"; +import { useGrids } from "@/core/hooks/useGrids"; +import { useAppSelector } from "@/core/redux/hooks"; + +export default function GridsPage() { + const router = useRouter(); + const activeNetwork = useAppSelector((state) => state.user.activeNetwork); + const { grids, isLoading: isGridsLoading } = useGrids( + activeNetwork?.net_name ?? "" + ); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const rowsPerPage = 8; + + const filteredGrids = grids.filter( + (grid) => + grid.name.toLowerCase().includes(searchQuery.toLowerCase()) || + grid.admin_level.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const totalPages = Math.ceil(filteredGrids.length / rowsPerPage); + const pages = Array.from({ length: totalPages }, (_, i) => i + 1); + + const renderPaginationNumbers = () => { + const pageNumbers = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + return pages.map((page) => ( + + )); + } + + // Always show first page + pageNumbers.push( + + ); + + // Calculate start and end of middle section + const startPage = Math.max(2, currentPage - 1); + const endPage = Math.min(totalPages - 1, currentPage + 1); + + // Add ellipsis after first page if needed + if (startPage > 2) { + pageNumbers.push( + + ... + + ); + } + + // Add middle pages + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push( + + ); + } + + // Add ellipsis before last page if needed + if (endPage < totalPages - 1) { + pageNumbers.push( + + ... + + ); + } + + // Always show last page + if (totalPages > 1) { + pageNumbers.push( + + ); + } + + return pageNumbers; + }; + + if (isGridsLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

Grid Registry

+

+ Manage and organize your monitoring grids +

+
+
+ + +
+
+ +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ + + + Grid Name + Number of sites + Admin level + Visibility + Date created + + + + {filteredGrids + .slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage) + .map((grid) => ( + router.push(`/grids/${grid._id}`)} + > + {grid.name} + + {grid.numberOfSites} + + {grid.admin_level} + + + {grid.visibility ? "Visible" : "Hidden"} + + + + {new Date(grid.createdAt).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} + + + ))} + +
+
+
+ +
+ {renderPaginationNumbers()} +
+ +
+
+ ); +} diff --git a/netmanager-app/app/(authenticated)/sites/create-site-form.tsx b/netmanager-app/app/(authenticated)/sites/create-site-form.tsx index d0a3a137fb..f5dfed78b5 100644 --- a/netmanager-app/app/(authenticated)/sites/create-site-form.tsx +++ b/netmanager-app/app/(authenticated)/sites/create-site-form.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -26,7 +25,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Loader2, MapPin, Check } from "lucide-react"; +import { Loader2, Check } from "lucide-react"; import { cn } from "@/lib/utils"; import { sites } from "@/core/apis/sites"; import { useAppSelector } from "@/core/redux/hooks"; @@ -35,8 +34,6 @@ import { MapContainer, TileLayer, Marker, useMap } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import L from "leaflet"; import { useApproximateCoordinates } from "@/core/hooks/useSites"; -import { AxiosError } from "axios"; -import Error from "next/error"; const siteFormSchema = z.object({ name: z.string().min(2, { diff --git a/netmanager-app/app/types/grids.ts b/netmanager-app/app/types/grids.ts index d1c4bc2478..fd87641892 100644 --- a/netmanager-app/app/types/grids.ts +++ b/netmanager-app/app/types/grids.ts @@ -1,3 +1,4 @@ +import { Position } from "@/core/redux/slices/gridsSlice"; import { Site } from "./sites"; export interface CreateGrid { @@ -5,7 +6,7 @@ export interface CreateGrid { admin_level: string; shape: { type: "MultiPolygon" | "Polygon"; - coordinates: number[][][][]; + coordinates: Position[][] | Position[][][]; }; network: string; } diff --git a/netmanager-app/components/Analytics/index.tsx b/netmanager-app/components/Analytics/index.tsx index e40875704a..dc6e0be80b 100644 --- a/netmanager-app/components/Analytics/index.tsx +++ b/netmanager-app/components/Analytics/index.tsx @@ -20,7 +20,7 @@ import { useToast } from "@/components/ui/use-toast"; import { useAppSelector } from "@/core/redux/hooks"; import { Cohort } from "@/app/types/cohorts"; import { Grid } from "@/app/types/grids"; -import { useDevices } from "@/core/hooks/useDevices"; +import { useMapReadings } from "@/core/hooks/useDevices"; import { transformDataToGeoJson } from "@/lib/utils"; const NewAnalytics: React.FC = () => { @@ -43,7 +43,7 @@ const NewAnalytics: React.FC = () => { activeNetwork?.net_name ?? "" ); const { cohorts, isLoading: isCohortsLoading } = useCohorts(); - const { mapReadings, isLoading: isReadingsLoading } = useDevices(); + const { mapReadings, isLoading } = useMapReadings(); const airqloudsData = isCohort ? cohorts : grids; diff --git a/netmanager-app/components/Settings/MyProfile.tsx b/netmanager-app/components/Settings/MyProfile.tsx index dc7b2a663a..966f11e48f 100644 --- a/netmanager-app/components/Settings/MyProfile.tsx +++ b/netmanager-app/components/Settings/MyProfile.tsx @@ -58,7 +58,8 @@ export default function MyProfile() { setError(null); try { - const userData = currentUser; + const response = await users.getUserDetails(currentUser._id); + const userData = response?.users?.[0]; if (userData) { setProfile({ diff --git a/netmanager-app/components/cohorts/assign-cohort-devices.tsx b/netmanager-app/components/cohorts/assign-cohort-devices.tsx new file mode 100644 index 0000000000..4e40487990 --- /dev/null +++ b/netmanager-app/components/cohorts/assign-cohort-devices.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const devices = [ + { value: "aq_40", label: "Aq_40" }, + { value: "aq_41", label: "Aq_41" }, + { value: "aq_42", label: "Aq_42" }, + { value: "airqo_g5364", label: "Airqo_g5364" }, +]; + +const formSchema = z.object({ + devices: z.array(z.string()).min(1, { + message: "Please select at least one device.", + }), +}); + +export function AddDevicesDialog() { + const [open, setOpen] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + devices: [], + }, + }); + + function onSubmit(values: z.infer) { + console.log(values); + setOpen(false); + } + + return ( + + + + + + + Assign devices to cohort + +
+ + ( + + + + + + + + + + + + No devices found. + + {devices.map((device) => ( + { + const current = new Set(field.value); + if (current.has(device.value)) { + current.delete(device.value); + } else { + current.add(device.value); + } + field.onChange(Array.from(current)); + }} + > + + {device.label} + + ))} + + + + + + + + )} + /> +
+ + +
+ + +
+
+ ); +} diff --git a/netmanager-app/components/cohorts/create-cohort.tsx b/netmanager-app/components/cohorts/create-cohort.tsx new file mode 100644 index 0000000000..746847f0e9 --- /dev/null +++ b/netmanager-app/components/cohorts/create-cohort.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const devices = [ + { value: "aq_29", label: "Aq_29" }, + { value: "aq_34", label: "Aq_34" }, + { value: "aq_35", label: "Aq_35" }, + { value: "airqo_g5363", label: "Airqo_g5363" }, +]; + +const formSchema = z.object({ + name: z.string().min(2, { + message: "Cohort name must be at least 2 characters.", + }), + network: z.string().min(1, { + message: "Please select a network.", + }), + devices: z.array(z.string()).min(1, { + message: "Please select at least one device.", + }), +}); + +export function CreateCohortDialog() { + const [open, setOpen] = useState(false); + // const [selectedDevices, setSelectedDevices] = useState([]) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + network: "airqo", + devices: [], + }, + }); + + function onSubmit(values: z.infer) { + console.log(values); + setOpen(false); + } + + return ( + + + + + + + Add New Cohort + + Create a new cohort by providing the details below. + + +
+ + ( + + Cohort name + + + + + + )} + /> + ( + + Network + + + + + + )} + /> + ( + + Select Device(s) + + + + + + + + + + + No devices found. + + {devices.map((device) => ( + { + const current = new Set(field.value); + if (current.has(device.value)) { + current.delete(device.value); + } else { + current.add(device.value); + } + field.onChange(Array.from(current)); + }} + > + + {device.label} + + ))} + + + + + + + + )} + /> +
+ + +
+ + +
+
+ ); +} diff --git a/netmanager-app/components/export-data/ExportForm.tsx b/netmanager-app/components/export-data/ExportForm.tsx index e6fdd37de3..91af641bdd 100644 --- a/netmanager-app/components/export-data/ExportForm.tsx +++ b/netmanager-app/components/export-data/ExportForm.tsx @@ -3,7 +3,13 @@ import { useState, useEffect } from "react"; import { useForm, Controller } from "react-hook-form"; import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { MultiSelect, Option } from "react-multi-select-component"; import { useToast } from "@/components/ui/use-toast"; import { ExportType, FormData } from "@/app/types/export"; @@ -18,9 +24,9 @@ import { useAppSelector } from "@/core/redux/hooks"; import { Grid } from "@/app/types/grids"; const pollutantOptions = [ - { value: "pm2.5", label: "PM2.5" }, - { value: "pm10", label: "PM10" }, - { value: "no2", label: "NO2" }, + { value: "pm2.5", label: "PM2.5" }, + { value: "pm10", label: "PM10" }, + { value: "no2", label: "NO2" }, ]; interface ExportFormProps { @@ -48,7 +54,6 @@ const options = { ], }; - export const roundToEndOfDay = (dateISOString: string): Date => { const end = new Date(dateISOString); end.setUTCHours(23, 59, 59, 999); @@ -63,22 +68,25 @@ const getvalue = (options: Option[]): string => { return options[0].value; }; - export const roundToStartOfDay = (dateISOString: string): Date => { const start = new Date(dateISOString); start.setUTCHours(0, 0, 0, 1); return start; }; - export default function ExportForm({ exportType }: ExportFormProps) { - const [loading, setLoading] = useState(false); - const { sites } = useSites(); - const { grids } = useGrids(); - const { devices } = useDevices(); - const activeNetwork = useAppSelector((state) => state.user.activeNetwork); - - const { control, handleSubmit, formState: { errors }, reset } = useForm({ + const [loading, setLoading] = useState(false); + const activeNetwork = useAppSelector((state) => state.user.activeNetwork); + const { sites } = useSites(); + const { grids } = useGrids(activeNetwork?.net_name || ""); + const { devices } = useDevices(); + + const { + control, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ defaultValues: { startDateTime: undefined, endDateTime: undefined, @@ -99,32 +107,49 @@ export default function ExportForm({ exportType }: ExportFormProps) { useEffect(() => { let isSubscribed = true; if (sites?.length) { - const newSiteOptions = sites.map((site: Site) => ({ value: site._id, label: site.name })); - if (isSubscribed && JSON.stringify(newSiteOptions) !== JSON.stringify(siteOptions)) { + const newSiteOptions = sites.map((site: Site) => ({ + value: site._id, + label: site.name, + })); + if ( + isSubscribed && + JSON.stringify(newSiteOptions) !== JSON.stringify(siteOptions) + ) { setSiteOptions(newSiteOptions); } - } if (grids?.length) { - const newCityOptions = grids.map((grid: Grid) => ({ value: grid._id, label: grid.name })); - if (isSubscribed && JSON.stringify(newCityOptions) !== JSON.stringify(cityOptions)) { + const newCityOptions = grids.map((grid: Grid) => ({ + value: grid._id, + label: grid.name, + })); + if ( + isSubscribed && + JSON.stringify(newCityOptions) !== JSON.stringify(cityOptions) + ) { setCityOptions(newCityOptions); } } - + if (devices?.length) { - const newDeviceOptions = devices.map((device: Device) => ({ value: device._id, label: device.name })); - if (isSubscribed && JSON.stringify(newDeviceOptions) !== JSON.stringify(deviceOptions)) { + const newDeviceOptions = devices.map((device: Device) => ({ + value: device._id, + label: device.name, + })); + if ( + isSubscribed && + JSON.stringify(newDeviceOptions) !== JSON.stringify(deviceOptions) + ) { setDeviceOptions(newDeviceOptions); } } return () => { - isSubscribed = false; + isSubscribed = false; }; }, [sites, grids, devices]); const exportData = (data: string, filename: string, type: string) => { - const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_'); + const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, "_"); const blob = new Blob([data], { type }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -144,13 +169,13 @@ export default function ExportForm({ exportType }: ExportFormProps) { if (response) { if (data.fileType === "csv") { if (typeof response !== "string") { - throw new Error('Invalid CSV data format.'); + throw new Error("Invalid CSV data format."); } exportData(response, filename, "text/csv;charset=utf-8;"); } if (data.fileType === "json") { - const jsonString = JSON.stringify(response.data) + const jsonString = JSON.stringify(response.data); exportData(jsonString, filename, "application/json"); } @@ -158,41 +183,42 @@ export default function ExportForm({ exportType }: ExportFormProps) { title: "Data exported successfully", description: "Your data has been exported and is ready for download.", variant: "success", - }) - + }); } else { toast({ title: "Error exporting data", - description: "An error occurred while exporting your data. Please try again later.", + description: + "An error occurred while exporting your data. Please try again later.", variant: "destructive", }); } - } catch (error: any) { console.error("Error exporting data", error); - let errorMessage + let errorMessage; if (error.response) { - if (error.response.status >= 500) { - errorMessage = "An error occurred while exporting your data. Please try again later."; - + errorMessage = + "An error occurred while exporting your data. Please try again later."; } else { - if (error.response.data.status === 'success') { + if (error.response.data.status === "success") { toast({ title: "Error exporting data", - description: 'No data found for the selected parameters', + description: "No data found for the selected parameters", variant: "default", - }) + }); return; } - errorMessage = typeof error.response.data.message === 'string' ? error.response.data : 'An error occurred while downloading data'; + errorMessage = + typeof error.response.data.message === "string" + ? error.response.data + : "An error occurred while downloading data"; } - - }else if (error.request) { - errorMessage = 'No response received from server';; + } else if (error.request) { + errorMessage = "No response received from server"; } else { - errorMessage = 'An error occurred while exporting your data. Please try again later.'; + errorMessage = + "An error occurred while exporting your data. Please try again later."; } toast({ @@ -203,13 +229,11 @@ export default function ExportForm({ exportType }: ExportFormProps) { } finally { setLoading(false); } - }; - const onSubmit = async (data: any) => { setLoading(true); - + if (data.startDateTime > data.endDateTime) { toast({ title: "Invalid date range", @@ -219,40 +243,41 @@ export default function ExportForm({ exportType }: ExportFormProps) { setLoading(false); return; } - - const Difference_In_Time = new Date(data.endDateTime).getTime() - new Date(data.startDateTime).getTime(); + + const Difference_In_Time = + new Date(data.endDateTime).getTime() - + new Date(data.startDateTime).getTime(); const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24); - + if (Difference_In_Days > 28) { toast({ title: "Invalid date range", - description: "For time periods greater than a month, please reduce the time difference to a week to avoid timeouts!", + description: + "For time periods greater than a month, please reduce the time difference to a week to avoid timeouts!", variant: "default", }); setLoading(false); return; } - + let body: FormData = { startDateTime: roundToStartOfDay(data.startDateTime).toISOString(), endDateTime: roundToEndOfDay(data.endDateTime).toISOString(), - sites: getValues(data.sites), + sites: getValues(data.sites), devices: getValues(data.devices || []), cities: getValues(data.cities || []), regions: getValues(data.regions || []), - network: activeNetwork?.net_name || '', + network: activeNetwork?.net_name || "", dataType: getvalue(data.dataType || []), pollutants: getValues(data.pollutants), - frequency:data.frequency, + frequency: data.frequency, fileType: data.fileType, outputFormat: data.outputFormat, minimum: true, }; - + await downloadData(body); }; - - const renderSelect = ( name: keyof FormData, @@ -311,11 +336,17 @@ export default function ExportForm({ exportType }: ExportFormProps) { const renderFieldBasedOnTab = () => { switch (exportType) { case "sites": - return renderMultiSelect("sites", siteOptions, "Select sites", { required: "At least one site must be selected" }); + return renderMultiSelect("sites", siteOptions, "Select sites", { + required: "At least one site must be selected", + }); case "devices": - return renderMultiSelect("devices", deviceOptions, "Select devices", { required: "At least one device must be selected" }); + return renderMultiSelect("devices", deviceOptions, "Select devices", { + required: "At least one device must be selected", + }); case "airqlouds": - return renderMultiSelect("cities", cityOptions, "Select Grids", { required: "At least one Grids must be selected" }); + return renderMultiSelect("cities", cityOptions, "Select Grids", { + required: "At least one Grids must be selected", + }); default: return null; } @@ -329,8 +360,11 @@ export default function ExportForm({ exportType }: ExportFormProps) { control={control} rules={{ required: "Start date is required" }} render={({ field }) => ( - - + )} /> ( - - + )} />
{renderFieldBasedOnTab()} - {renderMultiSelect("pollutants", pollutantOptions, "Select pollutants", { required: "At least one pollutant must be selected" })} + {renderMultiSelect( + "pollutants", + pollutantOptions, + "Select pollutants", + { required: "At least one pollutant must be selected" } + )}
- {renderSelect("frequency", options.frequency, "Select frequency", { required: "Frequency is required" })} - {renderSelect("fileType", options.fileType, "Select file type", { required: "File type is required" })} + {renderSelect("frequency", options.frequency, "Select frequency", { + required: "Frequency is required", + })} + {renderSelect("fileType", options.fileType, "Select file type", { + required: "File type is required", + })}
- {renderSelect("outputFormat", options.outputFormat, "Select output format", { required: "Output format is required" })} - {renderSelect("dataType", options.dataType, "Select data type", { required: "Data type is required" })} + {renderSelect( + "outputFormat", + options.outputFormat, + "Select output format", + { required: "Output format is required" } + )} + {renderSelect("dataType", options.dataType, "Select data type", { + required: "Data type is required", + })}
+ + + + Add New Grid + + Create a new monitoring grid by providing the details below. + + +
+
+ + ( + + Grid name + + + + + + )} + /> + ( + + Administrative level + + + + + + )} + /> + ( + + Shapefile + +