diff --git a/src/frontend/package.json b/src/frontend/package.json
index 57998cdd8e..e98f098e3a 100755
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -52,6 +52,7 @@
"@mui/lab": "^5.0.0-alpha.134",
"@mui/material": "^5.14.12",
"@mui/system": "^5.14.12",
+ "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^1.2.2",
diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml
index fe7b798c57..f3caeae63f 100644
--- a/src/frontend/pnpm-lock.yaml
+++ b/src/frontend/pnpm-lock.yaml
@@ -32,6 +32,9 @@ dependencies:
'@mui/system':
specifier: ^5.14.12
version: 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@17.0.67)(react@17.0.2)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.0.4
+ version: 1.0.4(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2)
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2)
@@ -2226,6 +2229,34 @@ packages:
react-dom: 17.0.2(react@17.0.2)
dev: false
+ /@radix-ui/react-checkbox@1.0.4(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2):
+ resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.23.1
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@17.0.67)(react@17.0.2)
+ '@radix-ui/react-context': 1.0.1(@types/react@17.0.67)(react@17.0.2)
+ '@radix-ui/react-presence': 1.0.1(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@17.0.67)(react@17.0.2)
+ '@radix-ui/react-use-previous': 1.0.1(@types/react@17.0.67)(react@17.0.2)
+ '@radix-ui/react-use-size': 1.0.1(@types/react@17.0.67)(react@17.0.2)
+ '@types/react': 17.0.67
+ '@types/react-dom': 17.0.21
+ react: 17.0.2
+ react-dom: 17.0.2(react@17.0.2)
+ dev: false
+
/@radix-ui/react-collection@1.0.3(@types/react-dom@17.0.21)(@types/react@17.0.67)(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies:
diff --git a/src/frontend/src/api/OrganisationService.ts b/src/frontend/src/api/OrganisationService.ts
index 25d7224db0..5453826222 100644
--- a/src/frontend/src/api/OrganisationService.ts
+++ b/src/frontend/src/api/OrganisationService.ts
@@ -78,24 +78,110 @@ export const PostOrganisationDataService: Function = (url: string, payload: any)
dispatch(
CommonActions.SetSnackBar({
open: true,
- message: 'Organization Successfully Created.',
+ message: 'Organization Request Submitted.',
variant: 'success',
duration: 2000,
}),
);
} catch (error: any) {
+ dispatch(OrganisationAction.PostOrganisationDataLoading(false));
dispatch(
CommonActions.SetSnackBar({
open: true,
- message: error.response.data.detail,
+ message: error.response.data.detail || 'Failed to create organization.',
variant: 'error',
duration: 2000,
}),
);
- dispatch(OrganisationAction.PostOrganisationDataLoading(false));
}
};
await postOrganisationData(url, payload);
};
};
+
+export const GetIndividualOrganizationService: Function = (url: string) => {
+ return async (dispatch) => {
+ const getOrganisationData = async (url) => {
+ try {
+ const getOrganisationDataResponse = await axios.get(url);
+ const response: GetOrganisationDataModel = getOrganisationDataResponse.data;
+ dispatch(OrganisationAction.SetIndividualOrganization(response));
+ } catch (error) {}
+ };
+ await getOrganisationData(url);
+ };
+};
+
+export const PatchOrganizationDataService: Function = (url: string, payload: any) => {
+ return async (dispatch) => {
+ dispatch(OrganisationAction.PostOrganisationDataLoading(true));
+
+ const patchOrganisationData = async (url, payload) => {
+ dispatch(OrganisationAction.SetOrganisationFormData(payload));
+
+ try {
+ const generateApiFormData = new FormData();
+ appendObjectToFormData(generateApiFormData, payload);
+
+ const patchOrganisationData = await axios.patch(url, payload, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ const resp: HomeProjectCardModel = patchOrganisationData.data;
+ dispatch(OrganisationAction.PostOrganisationDataLoading(false));
+ dispatch(OrganisationAction.postOrganisationData(resp));
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: 'Organization Updated Successfully.',
+ variant: 'success',
+ duration: 2000,
+ }),
+ );
+ } catch (error: any) {
+ dispatch(OrganisationAction.PostOrganisationDataLoading(false));
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: error.response.data.detail || 'Failed to update organization.',
+ variant: 'error',
+ duration: 2000,
+ }),
+ );
+ }
+ };
+
+ await patchOrganisationData(url, payload);
+ };
+};
+
+export const ApproveOrganizationService: Function = (url: string, organizationId: string) => {
+ return async (dispatch) => {
+ const approveOrganization = async (url) => {
+ try {
+ await axios.post(url, organizationId);
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: 'Organization approved successfully.',
+ variant: 'success',
+ duration: 2000,
+ }),
+ );
+ } catch (error) {
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: 'Failed to approve organization.',
+ variant: 'error',
+ duration: 2000,
+ }),
+ );
+ }
+ };
+ await approveOrganization(url);
+ };
+};
diff --git a/src/frontend/src/components/ApproveOrganization/ApproveOrganizationHeader.tsx b/src/frontend/src/components/ApproveOrganization/ApproveOrganizationHeader.tsx
new file mode 100644
index 0000000000..79b90db9ac
--- /dev/null
+++ b/src/frontend/src/components/ApproveOrganization/ApproveOrganizationHeader.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import AssetModules from '@/shared/AssetModules.js';
+import { useNavigate } from 'react-router-dom';
+
+const ApproveOrganizationHeader = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
APPROVE ORGANIZATION
+
+
navigate('/organisation')}
+ >
+
+
+
+
+ );
+};
+
+export default ApproveOrganizationHeader;
diff --git a/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx b/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx
new file mode 100644
index 0000000000..22d226431d
--- /dev/null
+++ b/src/frontend/src/components/ApproveOrganization/OrganizationForm.tsx
@@ -0,0 +1,107 @@
+import React, { useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+import { useParams } from 'react-router-dom';
+import InputTextField from '@/components/common/InputTextField';
+import TextArea from '@/components/common/TextArea';
+import Button from '@/components/common/Button';
+import { ApproveOrganizationService, GetIndividualOrganizationService } from '@/api/OrganisationService';
+import CoreModules from '@/shared/CoreModules';
+
+const OrganizationForm = () => {
+ const dispatch = useDispatch();
+ const params = useParams();
+ const organizationId = params.id;
+ const organisationFormData: any = CoreModules.useAppSelector((state) => state.organisation.organisationFormData);
+
+ useEffect(() => {
+ if (organizationId) {
+ dispatch(GetIndividualOrganizationService(`${import.meta.env.VITE_API_URL}/organisation/${organizationId}`));
+ }
+ }, [organizationId]);
+
+ const approveOrganization = () => {
+ dispatch(
+ ApproveOrganizationService(`${import.meta.env.VITE_API_URL}/organisation/approve`, { org_id: organizationId }),
+ );
+ };
+
+ return (
+
+
+
+ Organizational Details
+
+
+
+
{}}
+ fieldType="text"
+ />
+ {}}
+ fieldType="text"
+ />
+ {}}
+ fieldType="text"
+ />
+
+
+ {}} />
+
+
+
+ );
+};
+
+export default OrganizationForm;
diff --git a/src/frontend/src/components/CreateEditOrganization/ConsentDetailsForm.tsx b/src/frontend/src/components/CreateEditOrganization/ConsentDetailsForm.tsx
new file mode 100644
index 0000000000..47bb4620da
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/ConsentDetailsForm.tsx
@@ -0,0 +1,98 @@
+import React, { useState } from 'react';
+import { consentQuestions } from '@/constants/ConsentQuestions';
+import { CustomCheckbox } from '@/components/common/Checkbox';
+import RadioButton from '@/components/common/RadioButton';
+import Button from '@/components/common/Button';
+import useForm from '@/hooks/useForm';
+import CoreModules from '@/shared/CoreModules';
+import ConsentDetailsValidation from '@/components/CreateEditOrganization/validation/ConsentDetailsValidation';
+import { useNavigate } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { OrganisationAction } from '@/store/slices/organisationSlice';
+import InstructionsSidebar from '@/components/CreateEditOrganization/InstructionsSidebar';
+
+const ConsentDetailsForm = () => {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const consentDetailsFormData: any = CoreModules.useAppSelector((state) => state.organisation.consentDetailsFormData);
+
+ const submission = () => {
+ dispatch(OrganisationAction.SetConsentApproval(true));
+ dispatch(OrganisationAction.SetConsentDetailsFormData(values));
+ };
+
+ const { handleSubmit, handleCustomChange, values, errors }: any = useForm(
+ consentDetailsFormData,
+ submission,
+ ConsentDetailsValidation,
+ );
+
+ return (
+
+
+
+
+ Consent Details
+
+
+ {consentQuestions.map((question) => (
+
+
+
+ {question.question} {question.required && * }
+
+ {question.description && (
+
{question.description}
+ )}
+
+ {question.type === 'radio' ? (
+
{
+ handleCustomChange(question.id, value);
+ }}
+ className="fmtm-font-archivo fmtm-text-base fmtm-text-[#7A7676] fmtm-mt-1"
+ errorMsg={errors.give_consent}
+ />
+ ) : (
+
+ {question.options.map((option) => (
+
{
+ return checked
+ ? handleCustomChange(question.id, [...values[question.id], option.id])
+ : handleCustomChange(
+ question.id,
+ values[question.id].filter((value) => value !== option.id),
+ );
+ }}
+ />
+ ))}
+ {errors[question.id] && {errors[question.id]}
}
+
+ )}
+
+ ))}
+
+
+
+ navigate('/organisation')}
+ />
+
+
+
+
+ );
+};
+
+export default ConsentDetailsForm;
diff --git a/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx
new file mode 100644
index 0000000000..3a98c62ac3
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationForm.tsx
@@ -0,0 +1,262 @@
+import React, { useEffect, useRef, useState } from 'react';
+import Button from '@/components/common/Button';
+import InputTextField from '@/components/common/InputTextField';
+import TextArea from '@/components/common/TextArea';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { OrganisationAction } from '@/store/slices/organisationSlice';
+import useForm from '@/hooks/useForm';
+import CoreModules from '@/shared/CoreModules';
+import AssetModules from '@/shared/AssetModules';
+import OrganizationDetailsValidation from '@/components/CreateEditOrganization/validation/OrganizationDetailsValidation';
+import RadioButton from '@/components/common/RadioButton';
+import { useDispatch } from 'react-redux';
+import {
+ GetIndividualOrganizationService,
+ PatchOrganizationDataService,
+ PostOrganisationDataService,
+} from '@/api/OrganisationService';
+import { diffObject } from '@/utilfunctions/compareUtils';
+import InstructionsSidebar from '@/components/CreateEditOrganization/InstructionsSidebar';
+
+type optionsType = {
+ name: string;
+ value: string;
+ label: string;
+};
+
+const organizationTypeOptions: optionsType[] = [
+ { name: 'osm_community', value: 'osm_community', label: 'OSM Community' },
+ { name: 'company', value: 'company', label: 'Company' },
+ { name: 'non_profit', value: 'non_profit', label: 'Non-profit' },
+ { name: 'university', value: 'university', label: 'University' },
+ { name: 'other', value: 'other', label: 'Other' },
+];
+
+const CreateEditOrganizationForm = ({ organizationId }) => {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const inputFileRef = useRef(null);
+ const organisationFormData: any = CoreModules.useAppSelector((state) => state.organisation.organisationFormData);
+ const postOrganisationDataLoading: boolean = CoreModules.useAppSelector(
+ (state) => state.organisation.postOrganisationDataLoading,
+ );
+ const postOrganisationData: any = CoreModules.useAppSelector((state) => state.organisation.postOrganisationData);
+ const [previewSource, setPreviewSource] = useState('');
+
+ const submission = () => {
+ if (!organizationId) {
+ dispatch(PostOrganisationDataService(`${import.meta.env.VITE_API_URL}/organisation/`, values));
+ } else {
+ const changedValues = diffObject(organisationFormData, values);
+ if (Object.keys(changedValues).length > 0) {
+ dispatch(
+ PatchOrganizationDataService(
+ `${import.meta.env.VITE_API_URL}/organisation/${organizationId}/`,
+ changedValues,
+ ),
+ );
+ }
+ }
+ };
+
+ const { handleSubmit, handleChange, handleCustomChange, values, errors }: any = useForm(
+ organisationFormData,
+ submission,
+ OrganizationDetailsValidation,
+ );
+
+ const previewFile = (file: File) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file); //reads file as data url (base64 encoding)
+ reader.onload = () => {
+ if (reader) {
+ setPreviewSource(reader?.result);
+ }
+ };
+ };
+
+ // redirect to manage-org page after post success
+ useEffect(() => {
+ if (postOrganisationData) {
+ dispatch(OrganisationAction.postOrganisationData(null));
+ dispatch(OrganisationAction.SetOrganisationFormData({}));
+ dispatch(
+ OrganisationAction.SetConsentDetailsFormData({
+ give_consent: '',
+ review_documentation: [],
+ log_into: [],
+ participated_in: [],
+ }),
+ );
+ if (searchParams.get('popup') === 'true') {
+ window.close();
+ } else {
+ navigate('/organisation');
+ }
+ }
+ }, [postOrganisationData]);
+
+ useEffect(() => {
+ if (organizationId) {
+ dispatch(GetIndividualOrganizationService(`${import.meta.env.VITE_API_URL}/organisation/${organizationId}`));
+ }
+ }, [organizationId]);
+
+ return (
+
+ {!organizationId &&
}
+
+
+ Organizational Details
+
+
+
+
+ {!organizationId && (
+
+ )}
+
+
+
+
+ {!organizationId && (
+
{
+ handleCustomChange('organization_type', value);
+ }}
+ className="fmtm-text-base fmtm-text-[#7A7676] fmtm-mt-1"
+ errorMsg={errors.organization_type}
+ required
+ />
+ )}
+
+
Upload Logo
+
+
{
+ handleCustomChange('logo', e.target?.files?.[0]);
+ if (e.target?.files?.[0]) {
+ previewFile(e.target?.files?.[0]);
+ }
+ }}
+ accept="image/png, image/gif, image/jpeg"
+ />
+ {(previewSource || values.logo) && (
+
+
+
{
+ inputFileRef.current.value = '';
+ handleCustomChange('logo', '');
+ setPreviewSource('');
+ }}
+ />
+
+
+
+ )}
+
+
+
+
+
+ {!organizationId && (
+ dispatch(OrganisationAction.SetConsentApproval(false))}
+ />
+ )}
+
+
+
+
+ );
+};
+
+export default CreateEditOrganizationForm;
diff --git a/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationHeader.tsx b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationHeader.tsx
new file mode 100644
index 0000000000..ddfe59f86c
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/CreateEditOrganizationHeader.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import AssetModules from '@/shared/AssetModules.js';
+import { useNavigate } from 'react-router-dom';
+
+const CreateEditOrganizationHeader = ({ organizationId }: { organizationId: string }) => {
+ const navigate = useNavigate();
+ return (
+
+
+
+
+ {organizationId ? 'EDIT YOUR ORGANIZATION' : 'REGISTER YOUR ORGANIZATION'}
+
+
+
navigate('/organisation')}
+ >
+
+
+
+
+ );
+};
+
+export default CreateEditOrganizationHeader;
diff --git a/src/frontend/src/components/CreateEditOrganization/InstructionsSidebar.tsx b/src/frontend/src/components/CreateEditOrganization/InstructionsSidebar.tsx
new file mode 100644
index 0000000000..81e7e5f774
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/InstructionsSidebar.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+const InstructionsSidebar = () => {
+ return (
+
+
Request for the organization creation
+
+
+ Project creation and other advanced usage of the HOT Field Mapping Tasking Manager needs an organisation to be
+ created.
+
+
+ HOT reserves all rights to give, and remove, all roles and permissions on FMTM as well as alter or remove
+ projects as deemed necessary.
+
+
+ Your input will be collected, stored, and processed by the Humanitarian OpenStreetMap Team (HOT) for the
+ purpose of evaluating your application and hosting your organization’s presence. Factual data about your use
+ of the Tasking Manager may be published for HOT to promote and demonstrate use of the FMTM.{' '}
+
+ Please make sure the information you are providing are correct.
+
+
+ );
+};
+
+export default InstructionsSidebar;
diff --git a/src/frontend/src/components/CreateEditOrganization/validation/ConsentDetailsValidation.ts b/src/frontend/src/components/CreateEditOrganization/validation/ConsentDetailsValidation.ts
new file mode 100644
index 0000000000..15b9673e0d
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/validation/ConsentDetailsValidation.ts
@@ -0,0 +1,34 @@
+interface IConsentDetailsFormData {
+ give_consent: string;
+ review_documentation: [];
+ log_into: [];
+ participated_in: [];
+}
+
+interface ValidationErrors {
+ give_consent?: string;
+ review_documentation?: string;
+ log_into?: string;
+ participated_in?: string;
+}
+
+function ConsentDetailsValidation(values: IConsentDetailsFormData) {
+ const errors: ValidationErrors = {};
+
+ if (!values?.give_consent) {
+ errors.give_consent = 'Consent is required.';
+ }
+ if (values?.give_consent === 'no') {
+ errors.give_consent = 'To proceed, it is required that you provide consent.';
+ }
+ if (values?.review_documentation?.length < 3) {
+ errors.review_documentation = 'Please ensure that all checkboxes are marked.';
+ }
+ if (values?.log_into?.length < 2) {
+ errors.log_into = 'Please ensure that all checkboxes are marked.';
+ }
+
+ return errors;
+}
+
+export default ConsentDetailsValidation;
diff --git a/src/frontend/src/components/CreateEditOrganization/validation/OrganizationDetailsValidation.ts b/src/frontend/src/components/CreateEditOrganization/validation/OrganizationDetailsValidation.ts
new file mode 100644
index 0000000000..aa2982f70e
--- /dev/null
+++ b/src/frontend/src/components/CreateEditOrganization/validation/OrganizationDetailsValidation.ts
@@ -0,0 +1,71 @@
+interface OrganisationValues {
+ id: string;
+ logo: string;
+ name: string;
+ description: string;
+ url: string;
+ type: number;
+ odk_central_url: string;
+ odk_central_user: string;
+ odk_central_password: string;
+ email: string;
+ osm_profile: string;
+ organization_type: string;
+}
+interface ValidationErrors {
+ logo?: string;
+ name?: string;
+ description?: string;
+ url?: string;
+ type?: string;
+ odk_central_url?: string;
+ odk_central_user?: string;
+ odk_central_password?: string;
+ email?: string;
+ osm_profile?: string;
+ organization_type?: string;
+}
+
+function isValidUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+function OrganizationDetailsValidation(values: OrganisationValues) {
+ const errors: ValidationErrors = {};
+
+ if (!values?.name) {
+ errors.name = 'Name is Required.';
+ }
+
+ if (!values?.description) {
+ errors.description = 'Description is Required.';
+ }
+
+ if (!values?.id) {
+ if (!values?.url) {
+ errors.url = 'Organization Url is Required.';
+ } else if (!isValidUrl(values.url)) {
+ errors.url = 'Invalid URL.';
+ }
+ if (!values?.organization_type) {
+ errors.organization_type = 'Organization type is Required.';
+ }
+ }
+
+ if (values?.odk_central_url && !isValidUrl(values.odk_central_url)) {
+ errors.odk_central_url = 'Invalid URL.';
+ }
+
+ if (!values?.email) {
+ errors.email = 'Email is Required.';
+ }
+
+ return errors;
+}
+
+export default OrganizationDetailsValidation;
diff --git a/src/frontend/src/components/common/Button.tsx b/src/frontend/src/components/common/Button.tsx
index 6b05229db3..79b81319fa 100644
--- a/src/frontend/src/components/common/Button.tsx
+++ b/src/frontend/src/components/common/Button.tsx
@@ -23,7 +23,7 @@ const btnStyle = (btnType, className) => {
return `hover:fmtm-bg-gray-100 fmtm-flex fmtm-bg-white fmtm-px-4 fmtm-py-1 fmtm-border border-[#E0E0E0] fmtm-rounded-[8px] ${className}`;
case 'other':
- return `fmtm-py-1 fmtm-px-4 fmtm-text-red-600 fmtm-rounded-lg fmtm-border-[1px] fmtm-border-red-600 ${className}`;
+ return `fmtm-py-1 fmtm-px-4 fmtm-text-red-600 fmtm-rounded-lg fmtm-border-[1px] fmtm-border-red-600 hover:fmtm-text-red-700 hover:fmtm-border-red-700 ${className}`;
case 'disabled':
return `fmtm-py-1 fmtm-px-4 fmtm-text-white fmtm-rounded-lg fmtm-bg-gray-400 fmtm-cursor-not-allowed ${className}`;
diff --git a/src/frontend/src/components/common/Checkbox.tsx b/src/frontend/src/components/common/Checkbox.tsx
new file mode 100644
index 0000000000..0f0c4249d1
--- /dev/null
+++ b/src/frontend/src/components/common/Checkbox.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import * as React from 'react';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { Check } from 'lucide-react';
+import { cn } from '@/utilfunctions/shadcn';
+
+type CustomCheckboxType = {
+ label: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+};
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export const CustomCheckbox = ({ label, checked, onCheckedChange }: CustomCheckboxType) => {
+ return (
+
+ );
+};
diff --git a/src/frontend/src/components/common/InputTextField.tsx b/src/frontend/src/components/common/InputTextField.tsx
index 0fbfbc9843..5c695c9a00 100644
--- a/src/frontend/src/components/common/InputTextField.tsx
+++ b/src/frontend/src/components/common/InputTextField.tsx
@@ -6,6 +6,7 @@ export const blockInvalidChar = (e) => ['e', 'E', '+', '-'].includes(e.key) && e
interface IInputTextFieldProps {
id?: string;
label: string;
+ subLabel?: string | React.JSX.Element;
onChange: (event: React.ChangeEvent) => void;
onKeyDown?: (event: React.KeyboardEvent) => void;
errorMsg?: string;
@@ -25,6 +26,7 @@ interface IInputTextFieldProps {
function InputTextField({
id,
label,
+ subLabel,
onChange,
onKeyDown,
errorMsg,
@@ -42,9 +44,12 @@ function InputTextField({
}: IInputTextFieldProps) {
return (
-
-
{label}
- {required &&
*
}
+
+
+
{label}
+ {required &&
*
}
+
+ {subLabel &&
{subLabel}
}
void;
value: string;
errorMsg?: string;
+ className?: string;
+ required?: boolean;
}
-const RadioButton: React.FC
= ({ topic, options, direction, onChangeData, value, errorMsg }) => (
+const RadioButton: React.FC = ({
+ topic,
+ options,
+ direction,
+ onChangeData,
+ value,
+ errorMsg,
+ className,
+ required,
+}) => (
-
+ {topic && (
+
+
+ {topic} {required && * }
+
+
+ )}
{options.map((option) => {
return (
@@ -38,7 +53,7 @@ const RadioButton: React.FC
= ({ topic, options, direction, on
/>
{option.label}
{option.icon && option.icon}
diff --git a/src/frontend/src/constants/ConsentQuestions.tsx b/src/frontend/src/constants/ConsentQuestions.tsx
new file mode 100644
index 0000000000..08c86b9bb8
--- /dev/null
+++ b/src/frontend/src/constants/ConsentQuestions.tsx
@@ -0,0 +1,144 @@
+import React from 'react';
+
+type consentQuestionsType = {
+ id: string;
+ type: 'radio' | 'checkbox';
+ required: boolean;
+ question: string;
+ description: string | null;
+ options: any[];
+};
+
+export const consentQuestions: consentQuestionsType[] = [
+ {
+ id: 'give_consent',
+ type: 'radio',
+ required: true,
+ question: 'Do you give consent?',
+ description: null,
+ options: [
+ {
+ name: 'give_consent',
+ value: 'yes',
+ label: 'Yes, I agree to provide consent to collect, store and process information I provide',
+ },
+ {
+ name: 'give_consent',
+ value: 'no',
+ label: 'No, I do not agree to provide consent to collect, store and process information I provide',
+ },
+ ],
+ },
+ {
+ id: 'review_documentation',
+ type: 'checkbox',
+ required: true,
+ question: 'Please review the following documentation...?',
+ description: 'Check each box after you have reviewed the material',
+ options: [
+ {
+ id: 'code_of_conduct',
+ label: (
+
+ Code of Conduct
+
+ {' '}
+ https://www.hotosm.org/code-of-conduct
+
+
+ ),
+ },
+ {
+ id: 'learn_osm',
+ label: (
+
+ LearnOSM
+
+ {' '}
+ https://learnosm.org/en/coordination/humanitarian/{' '}
+
+ and associated modules
+
+ ),
+ },
+ {
+ id: 'compliance_guide',
+ label: (
+
+ HOT Suggested OEG Compliance Guide{' '}
+
+ {' '}
+ https://docs.google.com/document/d/1IIrR75Cmy92giXLa9hCVIur0wJ3HU4nTZoq6zQWyrEU/edit?usp=sharing
+
+
+ ),
+ },
+ ],
+ },
+ {
+ id: 'log_into',
+ type: 'checkbox',
+ required: true,
+ question: 'Also, please log-into...?',
+ description:
+ 'You do not have to map/take any action - logging in adds your username to the TM database for permissions, etc.',
+ options: [
+ {
+ id: 'staging_site',
+ label: (
+
+ The staging site{' '}
+
+ {' '}
+ https://stage.fmtm.hotosm.org/
+
+
+ ),
+ },
+ {
+ id: 'main_site',
+ label: (
+
+ The main Field Mapping Tasking Manager{' '}
+
+ https://fmtm.hotosm.org/
+
+
+ ),
+ },
+ ],
+ },
+ {
+ id: 'participated_in',
+ type: 'checkbox',
+ required: true,
+ question: 'Have you participated in...?',
+ description: 'These are not required but helpful in assessing on-boarding pathways.',
+ options: [
+ { id: 'mapathon', label: 'Participated in a Mapathon (in person or remote)' },
+ { id: 'field_survey', label: 'Organized or helped facilitate a Field Survey (in person or remote)' },
+ { id: 'validation_qa', label: 'Organized or helped coordinate a validation / quality assurance effort' },
+ { id: 'tm_josm', label: 'Contributed to OpenStreetMap via different mapping tools such as TM, JOSM' },
+ ],
+ },
+];
diff --git a/src/frontend/src/routes.jsx b/src/frontend/src/routes.jsx
index f0fb30fe4f..21132600f6 100755
--- a/src/frontend/src/routes.jsx
+++ b/src/frontend/src/routes.jsx
@@ -11,6 +11,8 @@ import ProtectedRoute from '@/utilities/ProtectedRoute';
import NotFoundPage from '@/views/NotFound404';
import Organisation from '@/views/Organisation';
import CreateOrganisation from '@/views/CreateOrganisation';
+import CreateEditOrganization from '@/views/CreateEditOrganization';
+import ApproveOrganization from '@/views/ApproveOrganization';
import Authorized from '@/views/Authorized';
import SubmissionDetails from '@/views/SubmissionDetails';
import CreateNewProject from '@/views/CreateNewProject';
@@ -55,6 +57,30 @@ const routes = createBrowserRouter([
),
},
+ {
+ path: '/create-organization',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/edit-organization/:id',
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: '/approve-organization/:id',
+ element: (
+
+
+
+ ),
+ },
// {
// path: '/explore',
// element: ,
diff --git a/src/frontend/src/store/slices/organisationSlice.ts b/src/frontend/src/store/slices/organisationSlice.ts
index 30af04dbbb..bd1caaa287 100644
--- a/src/frontend/src/store/slices/organisationSlice.ts
+++ b/src/frontend/src/store/slices/organisationSlice.ts
@@ -8,6 +8,13 @@ const OrganisationSlice = CoreModules.createSlice({
postOrganisationData: null,
organisationDataLoading: false,
postOrganisationDataLoading: false,
+ consentDetailsFormData: {
+ give_consent: '',
+ review_documentation: [],
+ log_into: [],
+ participated_in: [],
+ },
+ consentApproval: false,
},
reducers: {
GetOrganisationsData(state, action) {
@@ -25,6 +32,15 @@ const OrganisationSlice = CoreModules.createSlice({
SetOrganisationFormData(state, action) {
state.organisationFormData = action.payload;
},
+ SetConsentDetailsFormData(state, action) {
+ state.consentDetailsFormData = action.payload;
+ },
+ SetConsentApproval(state, action) {
+ state.consentApproval = action.payload;
+ },
+ SetIndividualOrganization(state, action) {
+ state.organisationFormData = action.payload;
+ },
},
});
diff --git a/src/frontend/src/types/enums.ts b/src/frontend/src/types/enums.ts
index e499f3a5a7..07636cc712 100644
--- a/src/frontend/src/types/enums.ts
+++ b/src/frontend/src/types/enums.ts
@@ -15,3 +15,9 @@ export enum task_priority_str {
SPLIT = 7,
ARCHIVED = 8,
}
+
+export enum user_roles {
+ READ_ONLY = '-1',
+ MAPPER = '0',
+ ADMIN = '1',
+}
diff --git a/src/frontend/src/utilfunctions/login.ts b/src/frontend/src/utilfunctions/login.ts
index 185e0c7a03..7ed72970e7 100644
--- a/src/frontend/src/utilfunctions/login.ts
+++ b/src/frontend/src/utilfunctions/login.ts
@@ -44,6 +44,7 @@ export const createLoginWindow = (redirectTo) => {
id: userRes.id,
picture: userRes.img_url,
redirect_to: redirectTo,
+ role: userRes.role,
}).toString();
const redirectUrl = `/osmauth?${params}`;
window.location.href = redirectUrl;
diff --git a/src/frontend/src/views/ApproveOrganization.tsx b/src/frontend/src/views/ApproveOrganization.tsx
new file mode 100644
index 0000000000..02887ab142
--- /dev/null
+++ b/src/frontend/src/views/ApproveOrganization.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import ApproveOrganizationHeader from '@/components/ApproveOrganization/ApproveOrganizationHeader';
+import OrganizationForm from '@/components/ApproveOrganization/OrganizationForm';
+
+const ApproveOrganization = () => {
+ return (
+
+ );
+};
+
+export default ApproveOrganization;
diff --git a/src/frontend/src/views/Authorized.tsx b/src/frontend/src/views/Authorized.tsx
index 2204e32314..59440dcb40 100644
--- a/src/frontend/src/views/Authorized.tsx
+++ b/src/frontend/src/views/Authorized.tsx
@@ -29,8 +29,9 @@ function Authorized() {
const sessionToken = params.get('session_token');
const osm_oauth_token = params.get('osm_oauth_token');
const picture = params.get('picture');
+ const role = params.get('role');
dispatch(LoginActions.setAuthDetails(username, sessionToken, osm_oauth_token));
- dispatch(LoginActions.SetLoginToken({ username, id, sessionToken, osm_oauth_token, picture }));
+ dispatch(LoginActions.SetLoginToken({ username, id, sessionToken, osm_oauth_token, picture, role }));
const redirectUrl = params.get('redirect_to') || '/';
setIsReadyToRedirect(true);
diff --git a/src/frontend/src/views/CreateEditOrganization.tsx b/src/frontend/src/views/CreateEditOrganization.tsx
new file mode 100644
index 0000000000..c6ec025083
--- /dev/null
+++ b/src/frontend/src/views/CreateEditOrganization.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import CoreModules from '@/shared/CoreModules';
+import environment from '@/environment';
+import CreateEditOrganizationHeader from '@/components/CreateEditOrganization/CreateEditOrganizationHeader';
+import ConsentDetailsForm from '@/components/CreateEditOrganization/ConsentDetailsForm';
+import CreateEditOrganizationForm from '@/components/CreateEditOrganization/CreateEditOrganizationForm';
+
+const CreateEditOrganization = () => {
+ const params = CoreModules.useParams();
+ const organizationId = params.id;
+ const consentApproval: any = CoreModules.useAppSelector((state) => state.organisation.consentApproval);
+
+ return (
+
+
+
+ {organizationId || (!organizationId && consentApproval) ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export default CreateEditOrganization;
diff --git a/src/frontend/src/views/Organisation.tsx b/src/frontend/src/views/Organisation.tsx
index 4507371b22..0bc391cbbd 100644
--- a/src/frontend/src/views/Organisation.tsx
+++ b/src/frontend/src/views/Organisation.tsx
@@ -1,23 +1,28 @@
import React, { useEffect, useState } from 'react';
import CoreModules from '@/shared/CoreModules';
import AssetModules from '@/shared/AssetModules';
-import environment from '@/environment';
import { OrganisationDataService } from '@/api/OrganisationService';
+import { user_roles } from '@/types/enums';
+import CustomizedImage from '@/utilities/CustomizedImage';
+import { GetOrganisationDataModel } from '@/models/organisation/organisationModel';
const Organisation = () => {
const cardStyle = {
- padding: 2,
+ padding: '20px',
display: 'flex',
flexDirection: 'row',
- alignItems: 'center',
cursor: 'pointer',
- gap: 5,
+ gap: '20px',
+ boxShadow: 'none',
+ borderRadius: '0px',
};
const url = 'https://fmtm.naxa.com.np/d907cf67fe587072a592.png';
- const [searchKeyword, setSearchKeyword] = useState('');
- const [activeTab, setActiveTab] = useState(0);
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [activeTab, setActiveTab] = useState<0 | 1>(0);
+ const [verifiedTab, setVerifiedTab] = useState(false);
+ const token = CoreModules.useAppSelector((state) => state.login.loginToken);
const handleSearchChange = (event) => {
setSearchKeyword(event.target.value);
@@ -25,9 +30,10 @@ const Organisation = () => {
const dispatch = CoreModules.useAppDispatch();
- const oraganizationData: any = CoreModules.useAppSelector((state) => state.organisation.oraganizationData);
- console.log(oraganizationData, 'oraganizationData');
- const filteredCardData = oraganizationData?.filter((data) =>
+ const oraganizationData: GetOrganisationDataModel[] = CoreModules.useAppSelector(
+ (state) => state.organisation.oraganizationData,
+ );
+ const filteredCardData: GetOrganisationDataModel[] = oraganizationData?.filter((data) =>
data.name.toLowerCase().includes(searchKeyword.toLowerCase()),
);
@@ -44,54 +50,109 @@ const Organisation = () => {
flex: 1,
gap: 2,
}}
- className="fmtm-px-[4.5%]"
+ className="fmtm-p-5"
>
-
- MANAGE ORGANIZATIONS
-
- }
- sx={{ minWidth: 'fit-content', width: 'auto', fontWeight: 'bold' }}
- >
- New
-
-
-
-
-
- setActiveTab(0)}
- />
- setActiveTab(1)}
- />
-
-
+
+
+
MANAGE ORGANIZATIONS
+
+
+
+
+
+ setActiveTab(0)}
+ />
+ setActiveTab(1)}
+ />
+
+ }
+ sx={{
+ marginLeft: ['8px', '12px', '12px'],
+ minWidth: 'fit-content',
+ width: 'auto',
+ fontWeight: 'bold',
+ minHeight: ['26px', '36px', '36px'],
+ height: ['30px', '36px', '36px'],
+ px: ['12px', '16px', '16px'],
+ }}
+ >
+ New
+
+
+
+
+ {token !== null && token['role'] && token['role'] === user_roles.ADMIN && (
+
+
+ setVerifiedTab(false)}
+ />
+ setVerifiedTab(true)}
+ />
+
+
+ )}
+
{
className="fmtm-min-w-[14rem] lg:fmtm-w-[20%]"
/>
-
+
+
+ Showing {filteredCardData?.length} of {oraganizationData?.length} organizations
+
+
+
{filteredCardData?.map((data, index) => (
-
-
-
+
+
+ ) : (
+
+
+
+ )}
+
+
+
{data.name}
-
-
+
{data.description}
-
-
-
- {!data.logo || data.logo === 'string' ? data.name[0] : url}
-
-
+
))}
diff --git a/src/frontend/src/views/ProjectDetailsV2.tsx b/src/frontend/src/views/ProjectDetailsV2.tsx
index 24fe681544..f634271a05 100644
--- a/src/frontend/src/views/ProjectDetailsV2.tsx
+++ b/src/frontend/src/views/ProjectDetailsV2.tsx
@@ -381,7 +381,7 @@ const Home = () => {
/>