@@ -85,10 +87,17 @@ const CustomiseLocationsComponent = ({ toggleCustomise }) => {
-
Customise
+
+ Customise
+
+
+
+
toggleCustomise()}>
@@ -97,7 +106,7 @@ const CustomiseLocationsComponent = ({ toggleCustomise }) => {
- Select at least 4 locations you would like to feature on your overview page.
+ Select any 4 locations you would like to feature on your overview page.
@@ -123,7 +132,7 @@ const CustomiseLocationsComponent = ({ toggleCustomise }) => {
)}
{/* TODO: Pollutant component and post selection to user defaults */}
-
+
-
+
- {activeGroup && activeGroup?.grp_title && activeGroup?.grp_title.length > 16
- ? activeGroup?.grp_title.slice(0, 16) + '...'
- : activeGroup?.grp_title}
+ {activeGroup && activeGroup?.grp_title && activeGroup?.grp_title.length > 14
+ ? formatString(activeGroup?.grp_title.slice(0, 14)) + '...'
+ : formatString(activeGroup?.grp_title)}
@@ -70,10 +132,10 @@ const OrganizationDropdown = () => {
}
id='options'
+ dropdownWidth='224px'
dropStyle={{
top: '41px',
zIndex: 999,
- width: '224px',
maxHeight: '320px',
overflowY: 'scroll',
overflowX: 'hidden',
@@ -82,10 +144,10 @@ const OrganizationDropdown = () => {
>
{userGroups.map((format) => (
handleDropdownSelect(format)}
- className={`w-full h-11 px-3.5 py-2.5 justify-between items-center inline-flex ${
+ className={`w-56 h-11 px-3.5 py-2.5 justify-between items-center inline-flex ${
activeGroup &&
activeGroup?.grp_title === format?.grp_title &&
'bg-secondary-neutral-light-50'
@@ -102,13 +164,23 @@ const OrganizationDropdown = () => {
: splitNameIntoList(format?.grp_title)[0][0]}
-
- {format.grp_title}
+
+ {format && format.grp_title && format.grp_title.length > 24
+ ? formatString(format.grp_title.slice(0, 24)) + '...'
+ : formatString(format.grp_title)}
{activeGroup && activeGroup?.grp_title === format?.grp_title && (
)}
+ {loading && selectedGroup._id === format._id && (
+
+
+
+ )}
))}
diff --git a/platform/src/common/components/Layout/index.jsx b/platform/src/common/components/Layout/index.jsx
index 66d33d69dc..35f13f8ded 100644
--- a/platform/src/common/components/Layout/index.jsx
+++ b/platform/src/common/components/Layout/index.jsx
@@ -27,6 +27,7 @@ const Layout = ({ pageTitle = 'AirQo Analytics', children, topbarTitle, noBorder
const [collapsed, setCollapsed] = useState(
() => JSON.parse(localStorage.getItem('collapsed')) || false,
);
+ const cardCheckList = useSelector((state) => state.cardChecklist.cards);
// Fetching user preferences
useEffect(() => {
@@ -77,7 +78,7 @@ const Layout = ({ pageTitle = 'AirQo Analytics', children, topbarTitle, noBorder
}, [userInfo, userPreferences, dispatch]);
// Fetching user checklists
- useEffect(() => {
+ const fetchData = () => {
if (userInfo?._id && !localStorage.getItem('dataFetched')) {
dispatch(fetchUserChecklists(userInfo._id)).then((action) => {
if (fetchUserChecklists.fulfilled.match(action)) {
@@ -85,15 +86,14 @@ const Layout = ({ pageTitle = 'AirQo Analytics', children, topbarTitle, noBorder
if (payload && payload.length > 0) {
const { items } = payload[0];
dispatch(updateCards(items));
- localStorage.setItem('dataFetched', 'true');
- } else {
- localStorage.setItem('dataFetched', 'true');
- return;
}
+ localStorage.setItem('dataFetched', 'true');
}
});
}
- }, [dispatch, userInfo]);
+ };
+
+ useEffect(fetchData, [dispatch, userInfo]);
useEffect(() => {
localStorage.setItem('collapsed', collapsed);
diff --git a/platform/src/common/components/Modal/PrintReportModal.jsx b/platform/src/common/components/Modal/PrintReportModal.jsx
index 11d906db4e..7bf96b9bca 100644
--- a/platform/src/common/components/Modal/PrintReportModal.jsx
+++ b/platform/src/common/components/Modal/PrintReportModal.jsx
@@ -20,7 +20,7 @@ const PrintReportModal = ({
title,
format,
btnText,
- ModalType = 'share',
+ shareModel,
shareStatus,
}) => {
const [loading, setLoading] = useState(false);
@@ -32,6 +32,46 @@ const PrintReportModal = ({
const [emails, setEmails] = useState(['']);
const [emailErrors, setEmailErrors] = useState([]);
const userInfo = useSelector((state) => state.login.userInfo);
+ // State for selected columns
+ const [selectedColumns, setSelectedColumns] = useState({
+ site_name: true,
+ device_name: true,
+ pm2_5_calibrated_value: true,
+ pm10_calibrated_value: true,
+ device_latitude: true,
+ device_longitude: true,
+ frequency: true,
+ datetime: true,
+ });
+
+ // Function to handle checkbox change
+ const handleCheckboxChange = (column) => {
+ setSelectedColumns((prev) => ({ ...prev, [column]: !prev[column] }));
+ };
+
+ // Array of table columns
+ const tableColumns = [
+ 'site_name',
+ 'device_name',
+ 'pm2_5_calibrated_value',
+ 'pm10_calibrated_value',
+ 'device_latitude',
+ 'device_longitude',
+ 'frequency',
+ 'datetime',
+ ];
+
+ // Mapping of column names
+ const columnNamesMapping = {
+ site_name: 'Site Name',
+ device_name: 'Device Name',
+ pm2_5_calibrated_value: 'PM2.5 Value',
+ pm10_calibrated_value: 'PM10 Value',
+ device_latitude: 'Latitude',
+ device_longitude: 'Longitude',
+ frequency: 'Frequency',
+ datetime: 'Date & Time',
+ };
const handleEmailChange = (index, value) => {
const updatedEmails = [...emails];
@@ -71,6 +111,16 @@ const PrintReportModal = ({
setEmails(['']);
setEmailErrors([]);
onClose();
+ setSelectedColumns({
+ site_name: true,
+ device_name: true,
+ pm2_5_calibrated_value: true,
+ pm10_calibrated_value: true,
+ device_latitude: true,
+ device_longitude: true,
+ frequency: true,
+ datetime: true,
+ });
};
const downloadDataFunc = () => {
@@ -103,23 +153,17 @@ const PrintReportModal = ({
// Function to generate CSV file
const generateCsv = (data) => {
- // convert data to array of objects
const dataArr = data.map((row) => {
- return {
- SiteName: row.site_name,
- PM2_5: row.pm2_5_calibrated_value,
- PM10: row.pm10_calibrated_value,
- Latitude: row.device_latitude,
- Longitude: row.device_longitude,
- Frequency: row.frequency,
- Date: row.datetime,
- };
+ const dataRow = {};
+ Object.keys(selectedColumns).forEach((column) => {
+ if (selectedColumns[column]) {
+ dataRow[columnNamesMapping[column]] = row[column];
+ }
+ });
+ return dataRow;
});
- // convert data to CSV
const csv = Papa.unparse(dataArr);
-
- // save CSV file
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -128,48 +172,40 @@ const PrintReportModal = ({
a.click();
window.URL.revokeObjectURL(url);
- // Return the CSV file
return new Blob([csv], { type: 'text/csv' });
};
// Function to generate PDF file
const generatePdf = (data) => {
const doc = new jsPDF('p', 'pt');
- const tableColumn = [
- 'SiteName',
- 'PM2.5 Value',
- 'PM10 Value',
- 'Latitude',
- 'Longitude',
- 'Frequency',
- 'Date',
- ];
const tableRows = [];
- // Loop through the data and push the rows to the tableRows array
data.forEach((row) => {
- const dataRow = [
- row.site_name,
- row.pm2_5_calibrated_value,
- row.pm10_calibrated_value,
- row.device_latitude,
- row.device_longitude,
- row.frequency,
- row.datetime,
- ];
+ const dataRow = {};
+ Object.keys(selectedColumns).forEach((column) => {
+ if (selectedColumns[column]) {
+ dataRow[columnNamesMapping[column]] = row[column];
+ }
+ });
tableRows.push(dataRow);
});
- // Set the table headers and data
- doc.autoTable(tableColumn, tableRows, { startY: 60 });
+ autoTable(doc, {
+ columns: Object.keys(selectedColumns)
+ .filter((column) => selectedColumns[column])
+ .map((col) => ({
+ header: columnNamesMapping[col],
+ dataKey: columnNamesMapping[col],
+ })),
+ body: tableRows,
+ startY: 60,
+ });
- // Add title and date
doc.text('Air quality data', 14, 15);
doc.text(`From: ${data[0].date} - To: ${data[data.length - 1].date}`, 14, 30);
doc.save('air_quality_data.pdf');
- // Return the PDF file
return doc.output('blob');
};
@@ -245,13 +281,12 @@ const PrintReportModal = ({
onClose;
handleCancel();
}}
- downloadDataFunc={ModalType === 'share' ? handleShareReport : handleDataExport}
+ downloadDataFunc={shareModel ? handleShareReport : handleDataExport}
loading={loading}
ModalIcon={ShareIcon}
primaryButtonText={btnText || 'Print'}
- data={data}
- >
- {ModalType === 'share' && (
+ data={data}>
+ {shareModel && (
<>
setAlert({ ...alert, show: false })}
/>
+
+
+
+ Deselect Columns for Report
+
+
+
+ {tableColumns.map((column, index) => (
+
+ handleCheckboxChange(column)}
+ className='form-checkbox h-5 w-5 text-blue-600 rounded'
+ />
+
+
+ ))}
+
+
@@ -271,7 +329,7 @@ const PrintReportModal = ({
handleEmailChange(index, e.target.value)}
/>
@@ -281,8 +339,7 @@ const PrintReportModal = ({
{index > 0 && (
)}
@@ -298,8 +355,7 @@ const PrintReportModal = ({
diff --git a/platform/src/core/apis/Account.js b/platform/src/core/apis/Account.js
index 0155c833c1..f9540f2f17 100644
--- a/platform/src/core/apis/Account.js
+++ b/platform/src/core/apis/Account.js
@@ -149,7 +149,7 @@ export const updateUserPreferencesApi = async (data) => {
}
}
-// Get Individual user preferences
+// Get Individual User preferences
export const getUserPreferencesApi = async (identifier) => {
try {
const response = await createAxiosInstance().get(`${USER_PREFERENCES_URL}/${identifier}`);
@@ -160,6 +160,18 @@ export const getUserPreferencesApi = async (identifier) => {
}
}
+// Patch/Replace User Preferences
+export const patchUserPreferencesApi = async (data) => {
+ try {
+ const response = await createAxiosInstance().patch(`${USER_PREFERENCES_URL}/replace`, data);
+ return response.data;
+ }
+ catch (error) {
+ throw error;
+ }
+}
+
+// User Defaults
export const getUserDefaults = async () => {
return await createAxiosInstance()
.get(USER_DEFAULTS_URL)
@@ -176,6 +188,7 @@ export const updateUserDefaults = async (defaultsId, defaults) => {
.then((response) => response.data);
};
+// User Checklist
export const getUserChecklists = async (userID) => {
return await createAxiosInstance()
.get(`${USER_CHECKLISTS_URL}/${userID}`)
@@ -188,6 +201,7 @@ export const upsertUserChecklists = async (checklist) => {
.then((response) => response.data);
};
+// Group Details
export const getGroupDetailsApi = async (groupID) => {
return await createAxiosInstance()
.get(`${GROUPS_URL}/${groupID}`)
diff --git a/platform/src/lib/store/services/account/UserDefaultsSlice.js b/platform/src/lib/store/services/account/UserDefaultsSlice.js
index b9a1fa3a12..25f0ef5949 100644
--- a/platform/src/lib/store/services/account/UserDefaultsSlice.js
+++ b/platform/src/lib/store/services/account/UserDefaultsSlice.js
@@ -1,4 +1,4 @@
-import { getUserPreferencesApi, postUserDefaultsApi, postUserPreferencesApi, updateUserDefaultsApi, updateUserPreferencesApi } from '@/core/apis/Account';
+import { getUserPreferencesApi, patchUserPreferencesApi, postUserDefaultsApi, postUserPreferencesApi, updateUserDefaultsApi, updateUserPreferencesApi } from '@/core/apis/Account';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
const initialState = {
@@ -47,6 +47,18 @@ export const getIndividualUserPreferences = createAsyncThunk('/get/individual/pr
}
})
+export const replaceUserPreferences = createAsyncThunk('/replace/individual/preference', async (data, { rejectWithValue }) => {
+ try {
+ const response = await patchUserPreferencesApi(data);
+ return response
+ } catch (error) {
+ if (!error.response) {
+ throw error
+ }
+ return rejectWithValue(error.response.data);
+ }
+})
+
export const defaultsSlice = createSlice({
name: 'defaults',
initialState,
@@ -90,6 +102,10 @@ export const defaultsSlice = createSlice({
state.errors = action.payload;
state.success = false;
})
+ .addCase(replaceUserPreferences.rejected, (state, action) => {
+ state.errors = action.payload;
+ state.success = false;
+ })
}
})
diff --git a/platform/src/pages/Home/index.jsx b/platform/src/pages/Home/index.jsx
index 9b27959137..385a024076 100644
--- a/platform/src/pages/Home/index.jsx
+++ b/platform/src/pages/Home/index.jsx
@@ -9,7 +9,7 @@ import AnalyticsImage from '@/images/Home/analyticsImage.png';
import PlayIcon from '@/images/Home/playIcon.svg';
import AnalyticsVideo from '../../../public/videos/analytics.mp4';
import { useSelector, useDispatch } from 'react-redux';
-import { startTask, updateTitle } from '@/lib/store/services/checklists/CheckList';
+import { startTask, completeTask, updateTitle } from '@/lib/store/services/checklists/CheckList';
import HomeSkeleton from '@/components/skeletons/HomeSkeleton';
import CustomModal from '@/components/Modal/videoModals/CustomModal';
import StepProgress from '@/components/steppers/CircularStepper';
@@ -48,17 +48,21 @@ const Home = () => {
};
const handleCardClick = (id) => {
- const card = cardCheckList.find((card) => card.id === id);
- if (card) {
- switch (card.status) {
- case 'not started':
- dispatch(startTask(id));
- break;
- default:
- return;
- }
+ if (id === 4) {
+ dispatch(completeTask(4));
} else {
- console.log('Card not found');
+ const card = cardCheckList.find((card) => card.id === id);
+ if (card) {
+ switch (card.status) {
+ case 'not started':
+ dispatch(startTask(id));
+ break;
+ default:
+ return;
+ }
+ } else {
+ console.log('Card not found');
+ }
}
};
@@ -84,7 +88,7 @@ const Home = () => {
{
label: 'Practical ways to reduce air pollution',
time: '1 min',
- link: '#',
+ link: 'https://blog.airqo.net/',
func: () => handleCardClick(4),
},
];
@@ -150,7 +154,10 @@ const Home = () => {
) : (
<>
-
+
{card && card.status === 'inProgress' ? 'Resume' : statusText}
diff --git a/platform/src/pages/_app.jsx b/platform/src/pages/_app.jsx
index aaf0f4d62f..e1266772d8 100644
--- a/platform/src/pages/_app.jsx
+++ b/platform/src/pages/_app.jsx
@@ -10,7 +10,10 @@ export default function App({ Component, ...rest }) {
const persistor = persistStore(store);
useEffect(() => {
- if (process.env.NEXT_PUBLIC_ALLOW_DEV_TOOLS === 'staging') {
+ if (
+ process.env.NEXT_PUBLIC_ALLOW_DEV_TOOLS === 'staging' ||
+ process.env.NEXT_PUBLIC_ALLOW_DEV_TOOLS === 'production'
+ ) {
return;
} else {
// Disable context menu (right click)
diff --git a/platform/src/pages/account/login/index.jsx b/platform/src/pages/account/login/index.jsx
index fbb681c680..9dd40e679b 100644
--- a/platform/src/pages/account/login/index.jsx
+++ b/platform/src/pages/account/login/index.jsx
@@ -21,6 +21,7 @@ const UserLogin = () => {
const postData = useSelector((state) => state.login);
const [loading, setLoading] = useState(false);
const [passwordType, setPasswordType] = useState('password');
+ const preferences = useSelector((state) => state.defaults.individual_preferences);
const handleLogin = async (e) => {
e.preventDefault();
@@ -45,11 +46,20 @@ const UserLogin = () => {
setLoading(false);
return;
}
- // find airqo group in the users groups and set it as the active group
- const airqoGroup = response.users[0].groups.find(
- (group) => group.grp_title === 'airqo',
- );
- localStorage.setItem('activeGroup', JSON.stringify(airqoGroup));
+
+ // check if user has a saved organisation
+ if (preferences && preferences[0] && preferences[0].group_id) {
+ const activeGroup = response.users[0].groups.find(
+ (group) => group._id === preferences[0].group_id,
+ );
+ localStorage.setItem('activeGroup', JSON.stringify(activeGroup));
+ } else {
+ const airqoGroup = response.users[0].groups.find(
+ (group) => group.grp_title === 'airqo',
+ );
+ localStorage.setItem('activeGroup', JSON.stringify(airqoGroup));
+ }
+
dispatch(setUserInfo(response.users[0]));
dispatch(setSuccess(true));
setLoading(false);
@@ -137,7 +147,8 @@ const UserLogin = () => {
diff --git a/platform/src/pages/analytics/index.jsx b/platform/src/pages/analytics/index.jsx
index e741a3857e..b0f6173f57 100644
--- a/platform/src/pages/analytics/index.jsx
+++ b/platform/src/pages/analytics/index.jsx
@@ -151,16 +151,14 @@ const AuthenticatedHomePage = () => {