diff --git a/src/vertex/components/features/grids/grid-measurements-api-card.tsx b/src/vertex/components/features/grids/grid-measurements-api-card.tsx
index 435ce88c90..90b736178d 100644
--- a/src/vertex/components/features/grids/grid-measurements-api-card.tsx
+++ b/src/vertex/components/features/grids/grid-measurements-api-card.tsx
@@ -5,7 +5,7 @@ import { Loader2 } from "lucide-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01 } from "@airqo/icons-react";
import { Grid } from "@/app/types/grids";
-import { useBanner } from "@/context/banner-context";
+import { useClipboard } from "@/core/hooks/useClipboard";
interface GridMeasurementsApiCardProps {
grid: Grid;
@@ -13,22 +13,12 @@ interface GridMeasurementsApiCardProps {
}
const GridMeasurementsApiCard: React.FC
= ({ grid, loading }) => {
- const { showBanner } = useBanner();
+ const { handleCopy } = useClipboard({ successMessage: 'API URL copied!', errorMessage: 'Failed to copy to clipboard' });
if (loading) {
return ;
}
- const handleCopy = async (text: string) => {
- if (!text) return;
- try {
- await navigator.clipboard.writeText(text);
- showBanner({ message: "API URL copied!", severity: "success", scoped: false });
- } catch {
- showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
- }
- };
-
const apiBase = (process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.airqo.net").replace(/\/$/, "");
const recentApiUrl = `${apiBase}/api/v2/devices/measurements/${grid._id}`;
const historicalApiUrl = `${apiBase}/api/v2/devices/measurements/grids/${grid._id}`;
diff --git a/src/vertex/components/features/shipping/PrepareShippingModal.tsx b/src/vertex/components/features/shipping/PrepareShippingModal.tsx
index ce1d75bdc4..6914c50ee4 100644
--- a/src/vertex/components/features/shipping/PrepareShippingModal.tsx
+++ b/src/vertex/components/features/shipping/PrepareShippingModal.tsx
@@ -4,9 +4,11 @@ import React, { useState, useRef } from 'react';
import { usePrepareBulkDevicesForShipping } from '@/core/hooks/useDevices';
import ReusableInputField from '@/components/shared/inputfield/ReusableInputField';
import ReusableButton from '@/components/shared/button/ReusableButton';
-import ReusableToast from '@/components/shared/toast/ReusableToast';
import { AqPlus, AqXClose, AqUploadCloud02 } from '@airqo/icons-react';
import ReusableDialog from '@/components/shared/dialog/ReusableDialog';
+import { useBanner } from '@/context/banner-context';
+import { useBannerWithDelay } from '@/core/hooks/useBannerWithDelay';
+import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage';
interface PrepareShippingModalProps {
isOpen: boolean;
@@ -22,13 +24,22 @@ export const PrepareShippingModal: React.FC = ({ isOp
const [filePreview, setFilePreview] = useState<{ headers: string[], data: unknown[][], fileName: string } | null>(null);
const [selectedColumn, setSelectedColumn] = useState(0);
const fileInputRef = useRef(null);
- const { mutate: prepareBulk, isPending } = usePrepareBulkDevicesForShipping();
+ const { showBanner } = useBanner();
+ const { showBannerWithDelay } = useBannerWithDelay();
+ const { mutate: prepareBulk, isPending } = usePrepareBulkDevicesForShipping({
+ onSuccess: (data) => {
+ showBannerWithDelay({ severity: 'success', message: data.message, scoped: false });
+ },
+ onError: (error) => {
+ showBanner({ severity: 'error', message: `Bulk Preparation Failed: ${getApiErrorMessage(error)}`, scoped: true });
+ },
+ });
const handleAddDevice = () => {
const trimmedInput = currentInput.trim();
if (!trimmedInput) return;
if (devices.includes(trimmedInput)) {
- ReusableToast({ message: 'Device already added', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Device already added', scoped: true });
return;
}
setDevices([...devices, trimmedInput]);
@@ -56,13 +67,13 @@ export const PrepareShippingModal: React.FC = ({ isOp
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (!['csv', 'xlsx', 'xls'].includes(fileExtension || '')) {
- ReusableToast({ message: 'Invalid file format. Please upload a CSV or Excel file.', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Invalid file format. Please upload a CSV or Excel file.', scoped: true });
e.target.value = '';
return;
}
if (file.size > 5 * 1024 * 1024) {
- ReusableToast({ message: 'File too large. Maximum size is 5MB.', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'File too large. Maximum size is 5MB.', scoped: true });
e.target.value = '';
return;
}
@@ -90,7 +101,7 @@ export const PrepareShippingModal: React.FC = ({ isOp
setIsImporting(false);
},
error: (error) => {
- ReusableToast({ message: `Error parsing CSV: ${error.message}`, type: 'ERROR' });
+ showBanner({ severity: 'error', message: `Error parsing CSV: ${error.message}`, scoped: true });
setIsImporting(false);
},
});
@@ -114,13 +125,13 @@ export const PrepareShippingModal: React.FC = ({ isOp
setIsImporting(false);
};
reader.onerror = () => {
- ReusableToast({ message: 'Error reading Excel file', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Error reading Excel file', scoped: true });
setIsImporting(false);
};
reader.readAsBinaryString(file);
}
} catch {
- ReusableToast({ message: 'Error importing file. Please ensure papaparse and xlsx libraries are installed.', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Error importing file. Please ensure papaparse and xlsx libraries are installed.', scoped: true });
setIsImporting(false);
}
@@ -146,7 +157,7 @@ export const PrepareShippingModal: React.FC = ({ isOp
const processImportedDevices = (importedDevices: string[]) => {
if (importedDevices.length === 0) {
- ReusableToast({ message: 'No valid device names found in the selected column', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'No valid device names found in the selected column', scoped: true });
return;
}
@@ -159,16 +170,16 @@ export const PrepareShippingModal: React.FC = ({ isOp
if (duplicateCount > 0) {
message += ` (${duplicateCount} duplicate${duplicateCount !== 1 ? 's' : ''} skipped)`;
}
- ReusableToast({ message, type: 'SUCCESS' });
+ showBanner({ severity: 'success', message, scoped: true });
} else if (duplicateCount > 0) {
- ReusableToast({ message: 'All devices in the file are already added', type: 'INFO' });
+ showBanner({ severity: 'info', message: 'All devices in the file are already added', scoped: true });
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (devices.length === 0) {
- ReusableToast({ message: 'Please add at least one device', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Please add at least one device', scoped: true });
return;
}
prepareBulk(
diff --git a/src/vertex/core/hooks/useClipboard.ts b/src/vertex/core/hooks/useClipboard.ts
new file mode 100644
index 0000000000..82a1624e15
--- /dev/null
+++ b/src/vertex/core/hooks/useClipboard.ts
@@ -0,0 +1,27 @@
+import { useBanner } from '@/context/banner-context';
+
+interface UseClipboardOptions {
+ successMessage?: string;
+ errorMessage?: string;
+ scoped?: boolean;
+}
+
+export const useClipboard = (options?: UseClipboardOptions) => {
+ const { showBanner } = useBanner();
+ const {
+ successMessage = 'Copied',
+ errorMessage = 'Failed to copy',
+ scoped = false,
+ } = options ?? {};
+
+ const handleCopy = async (text: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ showBanner({ severity: 'success', message: successMessage, scoped });
+ } catch {
+ showBanner({ severity: 'error', message: errorMessage, scoped });
+ }
+ };
+
+ return { handleCopy };
+};
diff --git a/src/vertex/core/hooks/useDevices.ts b/src/vertex/core/hooks/useDevices.ts
index 72958ea8c8..7e2c3c70d5 100644
--- a/src/vertex/core/hooks/useDevices.ts
+++ b/src/vertex/core/hooks/useDevices.ts
@@ -42,7 +42,7 @@ import type {
ShippingBatchDetailsResponse,
} from '@/app/types/devices';
import { AxiosError } from 'axios';
-import ReusableToast from '@/components/shared/toast/ReusableToast';
+import { useDispatch } from 'react-redux';
import { getApiErrorMessage } from '../utils/getApiErrorMessage';
import logger from '@/lib/logger';
@@ -292,7 +292,12 @@ export const useDeviceAvailability = (deviceName: string) => {
});
};
-export const useClaimDevice = () => {
+interface UseClaimDeviceOptions {
+ onSuccess?: () => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useClaimDevice = (options?: UseClaimDeviceOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -302,23 +307,22 @@ export const useClaimDevice = () => {
>({
mutationFn: devices.claimDevice,
onSuccess: () => {
- ReusableToast({
- message: 'Device Claimed Successfully!',
- type: 'SUCCESS',
- });
queryClient.invalidateQueries({ queryKey: ['myDevices'] });
queryClient.invalidateQueries({ queryKey: ['devices'] });
+ options?.onSuccess?.();
},
- onError: error => {
- ReusableToast({
- message: `Claim Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ onError: (error) => {
+ options?.onError?.(error);
},
});
};
-export const useBulkClaimDevices = () => {
+interface UseBulkClaimDevicesOptions {
+ onSuccess?: (data: BulkDeviceClaimResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useBulkClaimDevices = (options?: UseBulkClaimDevicesOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -327,36 +331,13 @@ export const useBulkClaimDevices = () => {
BulkDeviceClaimRequest
>({
mutationFn: devices.claimDevicesBulk,
- onSuccess: (response) => {
- const { successful_claims, failed_claims } = response.data;
- const successful_count = successful_claims.length;
- const failed_count = failed_claims.length;
-
- if (failed_count === 0) {
- ReusableToast({
- message: `All ${successful_count} device${successful_count !== 1 ? 's' : ''} claimed successfully!`,
- type: 'SUCCESS',
- });
- } else if (successful_count === 0) {
- ReusableToast({
- message: `Failed to claim all ${failed_count} device${failed_count !== 1 ? 's' : ''}`,
- type: 'ERROR',
- });
- } else {
- ReusableToast({
- message: `${successful_count} device${successful_count !== 1 ? 's' : ''} claimed, ${failed_count} failed`,
- type: 'WARNING',
- });
- }
-
+ onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['myDevices'] });
queryClient.invalidateQueries({ queryKey: ['devices'] });
+ options?.onSuccess?.(data);
},
- onError: error => {
- ReusableToast({
- message: `Bulk Claim Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ onError: (error) => {
+ options?.onError?.(error);
},
});
};
@@ -377,7 +358,12 @@ export const useAssignDeviceToOrganization = () => {
});
};
-export const useUnassignDeviceFromOrganization = () => {
+interface UseUnassignDeviceFromOrganizationOptions {
+ onSuccess?: (data: DeviceAssignmentResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useUnassignDeviceFromOrganization = (options?: UseUnassignDeviceFromOrganizationOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -387,19 +373,13 @@ export const useUnassignDeviceFromOrganization = () => {
>({
mutationFn: ({ deviceName, userId }) =>
devices.unassignDeviceFromOrganization(deviceName, userId),
- onSuccess: data => {
- ReusableToast({
- message: `${data.device.name} is now personal only.`,
- type: 'SUCCESS',
- });
+ onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['myDevices'] });
queryClient.invalidateQueries({ queryKey: ['devices'] });
+ options?.onSuccess?.(data);
},
- onError: error => {
- ReusableToast({
- message: `Unassignment Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ onError: (error) => {
+ options?.onError?.(error);
},
});
};
@@ -466,7 +446,12 @@ export interface BulkDeviceUpdatePayload {
updateData: Record;
}
-export const useUpdateDeviceBulk = () => {
+interface UseUpdateDeviceBulkOptions {
+ onSuccess?: () => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useUpdateDeviceBulk = (options?: UseUpdateDeviceBulkOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -478,53 +463,45 @@ export const useUpdateDeviceBulk = () => {
devices.bulkUpdateDeviceDetails(deviceIds, updateData),
onSuccess: () => {
- ReusableToast({
- message: "Devices updated successfully.",
- type: "SUCCESS",
- });
-
- // invalidate all relevant caches
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["myDevices"] });
queryClient.invalidateQueries({ queryKey: ["network-devices"] });
queryClient.invalidateQueries({ queryKey: ["deviceActivities"] });
+ options?.onSuccess?.();
},
onError: (error) => {
- ReusableToast({
- message: `Bulk Update Failed: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
},
});
};
-export const useUpdateDeviceGroup = () => {
+interface UseUpdateDeviceGroupOptions {
+ onSuccess?: () => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useUpdateDeviceGroup = (options?: UseUpdateDeviceGroupOptions) => {
+ const queryClient = useQueryClient();
return useMutation<
DeviceUpdateGroupResponse,
AxiosError,
{ deviceId: string; groupName: string }
>({
mutationFn: ({ deviceId, groupName }) =>
- devices.bulkUpdateDeviceDetails(
- [deviceId],
- {
- groups: [groupName],
- }
- ),
+ devices.bulkUpdateDeviceDetails([deviceId], { groups: [groupName] }),
onSuccess: () => {
- ReusableToast({
- message: "Device has been successfully added to the group.",
- type: "SUCCESS",
- });
+ queryClient.invalidateQueries({ queryKey: ["devices"] });
+ queryClient.invalidateQueries({ queryKey: ["myDevices"] });
+ queryClient.invalidateQueries({ queryKey: ["network-devices"] });
+ queryClient.invalidateQueries({ queryKey: ["deviceActivities"] });
+ options?.onSuccess?.();
+
},
onError: (error) => {
- ReusableToast({
- message: `Group Update Failed: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
},
});
};
@@ -766,7 +743,12 @@ export const useDecryptDeviceKeys = () => {
});
};
-export const usePrepareDeviceForShipping = () => {
+interface UsePrepareDeviceForShippingOptions {
+ onSuccess?: (data: PrepareDeviceResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const usePrepareDeviceForShipping = (options?: UsePrepareDeviceForShippingOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -777,22 +759,21 @@ export const usePrepareDeviceForShipping = () => {
mutationFn: ({ deviceName, tokenType }) =>
devices.prepareDeviceForShipping(deviceName, tokenType),
onSuccess: (data) => {
- ReusableToast({
- message: data.message,
- type: 'SUCCESS',
- });
queryClient.invalidateQueries({ queryKey: ['shippingStatus'] });
+ options?.onSuccess?.(data);
},
onError: (error) => {
- ReusableToast({
- message: `Preparation Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ options?.onError?.(error);
},
});
};
-export const usePrepareBulkDevicesForShipping = () => {
+interface UsePrepareBulkDevicesForShippingOptions {
+ onSuccess?: (data: BulkPrepareResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const usePrepareBulkDevicesForShipping = (options?: UsePrepareBulkDevicesForShippingOptions) => {
const queryClient = useQueryClient();
return useMutation<
@@ -803,34 +784,32 @@ export const usePrepareBulkDevicesForShipping = () => {
mutationFn: ({ deviceNames, tokenType, batchName }) =>
devices.prepareBulkDevicesForShipping(deviceNames, tokenType, batchName),
onSuccess: (data) => {
- ReusableToast({
- message: data.message,
- type: 'SUCCESS',
- });
- // queryClient.invalidateQueries({ queryKey: ['shippingStatus'] });
queryClient.invalidateQueries({ queryKey: ['shippingBatches'] });
+ options?.onSuccess?.(data);
},
onError: (error) => {
- ReusableToast({
- message: `Bulk Preparation Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ options?.onError?.(error);
},
});
};
-export const useGenerateShippingLabels = () => {
+interface UseGenerateShippingLabelsOptions {
+ onSuccess?: (data: GenerateLabelsResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useGenerateShippingLabels = (options?: UseGenerateShippingLabelsOptions) => {
return useMutation<
GenerateLabelsResponse,
AxiosError,
string[]
>({
mutationFn: (deviceNames) => devices.generateShippingLabels(deviceNames),
+ onSuccess: (data) => {
+ options?.onSuccess?.(data);
+ },
onError: (error) => {
- ReusableToast({
- message: `Label Generation Failed: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ options?.onError?.(error);
},
});
};