Recreate the org flow

This commit is contained in:
Ana Custura 2026-06-22 10:50:37 +01:00
parent ac733af3b5
commit 49e4bd8f8a
8 changed files with 233 additions and 107 deletions

View file

@ -19,6 +19,8 @@ import type {userObject} from "./hooks/UserContext.ts";
import Organisations from "./Organisations.tsx"; import Organisations from "./Organisations.tsx";
import {RefreshContext} from "./hooks/RefreshContext.ts"; import {RefreshContext} from "./hooks/RefreshContext.ts";
import Bridges from "./Bridges.tsx"; import Bridges from "./Bridges.tsx";
import {OrgContext, type OrgContextType, type OrgObject} from "./hooks/OrgContext.ts";
import CreateOrgFlow from "./CreateOrgFlow.tsx";
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
@ -72,9 +74,28 @@ const App: React.FC = () => {
}, },
setCurrentUser: () => {} setCurrentUser: () => {}
} }
const defaultOrg: OrgContextType = {
currentOrg: {
name: "",
status: "partial",
root_user_email: "",
billing_contact: {email: "", id: -1},
owner_contact: {email: "", id: -1},
security_contact: {email: "", id: -1},
organisation_id: -1,
intake_questionnaire: {
question_one: "",
question_two: "",
question_three: "",
},
},
setCurrentOrg: () => {}
}
const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser); const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser);
const [refreshKey, setRefreshKey] = useState<number>(1); const [refreshKey, setRefreshKey] = useState<number>(1);
const [currentOrg, setCurrentOrg] = useState<OrgObject>(defaultOrg.currentOrg);
/***************************** /*****************************
@ -132,7 +153,7 @@ const App: React.FC = () => {
navigate('/profile'); navigate('/profile');
break; break;
case 'orgs': case 'orgs':
navigate('/organisations'); navigate('/create-org-flow');
break; break;
case 'logout': case 'logout':
auth.removeUser().then(() => navigate('/profile')); auth.removeUser().then(() => navigate('/profile'));
@ -154,7 +175,7 @@ const App: React.FC = () => {
if (currentUser.organisations.length > 0) { if (currentUser.organisations.length > 0) {
side_nav_items = [ getItem('My organisations', 'orgs', <SettingOutlined />), side_nav_items = [ getItem('Onboarding', 'orgs', <SettingOutlined />),
getItem('Bridges', 'bridges', <DeploymentUnitOutlined />), ]; getItem('Bridges', 'bridges', <DeploymentUnitOutlined />), ];
const currentOrg = currentUser.organisations[0]['name'] const currentOrg = currentUser.organisations[0]['name']
@ -163,6 +184,7 @@ const App: React.FC = () => {
} }
return ( return (
<OrgContext.Provider value={{ currentOrg, setCurrentOrg }}>
<UserContext.Provider value={{ currentUser, setCurrentUser }}> <UserContext.Provider value={{ currentUser, setCurrentUser }}>
<RefreshContext.Provider value={{ refreshKey, setRefreshKey }}> <RefreshContext.Provider value={{ refreshKey, setRefreshKey }}>
<Layout> <Layout>
@ -209,6 +231,8 @@ const App: React.FC = () => {
<Route path="/" element={<Home/>}/> <Route path="/" element={<Home/>}/>
{currentUser && <Route path="/profile" element={<Profile/>}/> } {currentUser && <Route path="/profile" element={<Profile/>}/> }
{currentUser && <Route path="/organisations" element={<Organisations/>}/> } {currentUser && <Route path="/organisations" element={<Organisations/>}/> }
{currentUser && <Route path="/create-org-flow" element={<CreateOrgFlow/>}/> }
{currentUser && <Route path="/bridges" element={<Bridges/>}/> } {currentUser && <Route path="/bridges" element={<Bridges/>}/> }
</Routes> </Routes>
@ -218,6 +242,7 @@ const App: React.FC = () => {
</Layout> </Layout>
</RefreshContext.Provider> </RefreshContext.Provider>
</UserContext.Provider> </UserContext.Provider>
</OrgContext.Provider>
); );

View file

@ -4,8 +4,7 @@ import type { InputRef, TableColumnsType, TableColumnType } from 'antd';
import { Button, Input, Space, Table } from 'antd'; import { Button, Input, Space, Table } from 'antd';
import type { FilterDropdownProps } from 'antd/es/table/interface'; import type { FilterDropdownProps } from 'antd/es/table/interface';
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import { QRCode, theme } from 'antd'; import { QRCode } from 'antd';
const { useToken } = theme;
interface BridgeDataType { interface BridgeDataType {
fingerprint: string; fingerprint: string;
bridgeline: string; bridgeline: string;
@ -28,7 +27,6 @@ const Bridges = () => {
bridgeLines.forEach((line) => {line.qrCodeText = makeQRCodeText(line.bridgeline)}); bridgeLines.forEach((line) => {line.qrCodeText = makeQRCodeText(line.bridgeline)});
console.log(bridgeLines); console.log(bridgeLines);
const { token } = useToken();
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [searchedColumn, setSearchedColumn] = useState(''); const [searchedColumn, setSearchedColumn] = useState('');
@ -54,7 +52,7 @@ const Bridges = () => {
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> <div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
<Input <Input
ref={searchInput} //ref={searchInput}
placeholder={`Search ${dataIndex}`} placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]} value={selectedKeys[0]}
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])} onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
@ -146,7 +144,7 @@ const Bridges = () => {
width: '40%', width: '40%',
key: 'qrCodeText', key: 'qrCodeText',
dataIndex: 'qrCodeText', dataIndex: 'qrCodeText',
render: (text, record) => {return (<QRCode value={record.qrCodeText} />)} render: (_text, record) => {return (<QRCode value={record.qrCodeText} />)}
}, },
] ]

View file

@ -1,86 +0,0 @@
// src/Posts.jsx
import {useUserContext} from './hooks/UserContext.ts';
import { Button, Form, Input } from 'antd';
//import React, {useEffect, useState} from "react";
import {useAuth} from "react-oidc-context";
import {RefreshContext} from "./hooks/RefreshContext.ts";
import {useContext} from "react";
const CreateOrg = () => {
const { currentUser } = useUserContext();
const [form] = Form.useForm();
const auth = useAuth();
const { refreshKey, setRefreshKey } = useContext(RefreshContext);
console.log(refreshKey);
async function submitOrgData(values: FormData) {
if (!auth.user) {
return
}
try {
const [name, q1, q2, q3] = Object.values(values);
const data = {
"name": name,
"intake_questionnaire": {
"question_one": q1,
"question_two": q2,
"question_three": q3,
}
}
const token = auth.user.access_token;
const requestOptions = {
method: 'POST',
body: JSON.stringify(data),
headers: {Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'}
}
const response = await fetch("/api/v1/org", requestOptions);
await response.json();
setRefreshKey(prev => prev + 1);
console.log(refreshKey);
} catch (e) {
console.error(e);
}
}
const onFinish = (values: FormData) => {
console.log(values);
submitOrgData(values)
};
if (!currentUser) {
return <div>Loading...</div>;
}
return (
<>
<Form
layout= "vertical"
form={form}
onFinish={onFinish}
>
<Form.Item name="Organisation name" label="Organisation name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="Question 1" label="How does your organisation support civil society?">
<Input placeholder="e.g., Our organisation supports various groups such as ..." />
</Form.Item>
<Form.Item name="Question 2" label="Name your first reference">
<Input placeholder="e.g., SR2 Communciations Limited" />
</Form.Item>
<Form.Item name="Question 3" label="Name your second reference">
<Input placeholder="e.g., SR2 Professional Services" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</>
);
};
export default CreateOrg;

187
src/CreateOrgFlow.tsx Normal file
View file

@ -0,0 +1,187 @@
// src/Posts.jsx
import {useUserContext} from './hooks/UserContext.ts';
import { Button, Form, Input } from 'antd';
//import React, {useEffect, useState} from "react";
import {useAuth} from "react-oidc-context";
import {RefreshContext} from "./hooks/RefreshContext.ts";
import {useContext, useState} from "react";
import {useOrgContext} from "./hooks/OrgContext.ts";
const CreateOrgStep1 = () => {
const {currentUser} = useUserContext();
const [form] = Form.useForm();
const auth = useAuth();
const {refreshKey, setRefreshKey} = useContext(RefreshContext);
const {currentOrg, setCurrentOrg} = useOrgContext();
console.log('context value:', {currentOrg, setCurrentOrg});
const {TextArea} = Input;
const [currentStep, setCurrentStep] = useState(1);
async function submitOrgBasicData(values: FormData) {
if (!auth.user) {
return
}
try {
const [name, q1, q2] = Object.values(values);
const data = {
"name": name,
"intake_questionnaire": {
"question_one": q1,
"question_two": q2,
}
}
const token = auth.user.access_token;
const requestOptions = {
method: 'POST',
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
const response_org = await fetch("/api/v1/org", requestOptions);
const orgID = await response_org.json();
const response_currentOrg = await fetch("/api/v1/org?org_id=" + orgID['id'], {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const currentOrgData = await response_currentOrg.json()
setCurrentOrg(currentOrgData['organisations'][0]);
setCurrentStep(currentStep + 1);
} catch (e) {
console.error(e);
}
}
async function submitOrgBillingData(values: FormData) {
if (!auth.user) {
return
}
try {
const [first_name, last_name, email, phone_number, vat_number, street_address, street_address_line_2, locality] = Object.values(values);
const data = {
"organisation_id": currentOrg.organisation_id,
"contact_type": "billing",
"first_name": first_name,
"last_name": last_name,
"email": email,
"phone_number": phone_number,
"vat_number": vat_number,
"street_address": street_address,
"street_address_line_2": street_address_line_2,
"locality": locality,
}
const token = auth.user.access_token;
const requestOptions = {
method: 'PATCH',
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
await fetch("/api/v1/org/contact", requestOptions);
setCurrentStep(currentStep + 1);
setRefreshKey(refreshKey +1);
} catch (e) {
console.error(e);
}
}
if (!currentUser) {
return <div>Loading...</div>;
}
if (currentStep === 1) {
return (
<>
<h2>Step 1 - Basic info</h2>
<Form
layout="vertical"
form={form}
onFinish={submitOrgBasicData}
>
<Form.Item name="Organisation name" label="Organisation name" rules={[{required: true}]}>
<Input/>
</Form.Item>
<h4>Intake Questionnaire</h4>
<Form.Item name="Question 1" label="What region(s) does your organisation operate in?">
<TextArea showCount maxLength={100} placeholder="e.g., Southern Africa"/>
</Form.Item>
<Form.Item name="Question 2" label="Briefly describe how your organisation supports civil society.">
<TextArea showCount maxLength={300}
placeholder="e.g., Our organisation supports various groups such as ..."/>
</Form.Item>
<Form.Item name="Question 3" label="Name two references/contacts within the community, and briefly describe the nature of your relationsip and projects you've worked on together.">
<TextArea showCount maxLength={500}
placeholder="e.g., I worked with SR2 Communications on providing digital security training from 2023-2025."/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</>
);
}
if (currentStep === 2) {
return <>
<h2>Step 2 - Contact and billing details</h2>
<Form
layout="vertical"
form={form}
onFinish={submitOrgBillingData}
>
<Form.Item name="Billing contact first name" label="Billing contact first name"
rules={[{required: true}]}>
<Input/>
</Form.Item>
<Form.Item name="Billing contact last name" label="Billing contact last name"
rules={[{required: true}]}>
<Input/>
</Form.Item>
<Form.Item name="Billing contact email" label="Billing contact email address"
rules={[{required: true, type: "email", message: "Not a valid e-mail address."}]}>
<Input/>
</Form.Item>
<Form.Item name="Billing contact phone" label="Billing contact phone number">
<Input/>
</Form.Item>
<Form.Item name="Billing contact VAT number" label="Billing contact VAT number">
<Input/>
</Form.Item>
<Form.Item name="Billing address line 1" label="Billing address line 1" rules={[{required: true}]}>
<Input/>
</Form.Item>
<Form.Item name="Billing address line 2" label="Billing address line 2">
<Input/>
</Form.Item>
<Form.Item name="locality" label="City" rules={[{required: true}]}>
<Input/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</>
}
if (currentStep === 3) {
return <> <h2>Thank you for submitting your organisation info. </h2>
<p> This will be manually reviewed by a member of SR2 Cloud and you will be contacted shortly.
We aim to complete reviews within 24 hours. You can change your answers at any point from the
'My organisations' section.</p>
</>
}
}
export default CreateOrgStep1;

View file

@ -3,7 +3,6 @@ import OrgPanel from "./OrgPanel";
import {useAuth} from "react-oidc-context"; import {useAuth} from "react-oidc-context";
import type {OrgObject} from "./hooks/OrgContext.ts"; import type {OrgObject} from "./hooks/OrgContext.ts";
import type { JSX } from "react/jsx-runtime"; import type { JSX } from "react/jsx-runtime";
import CreateOrg from "./CreateOrg.tsx";
import {useRefreshContext} from "./hooks/RefreshContext.ts"; import {useRefreshContext} from "./hooks/RefreshContext.ts";
const Organisations: React.FC = () => { const Organisations: React.FC = () => {
@ -43,8 +42,6 @@ const Organisations: React.FC = () => {
return(<> return(<>
<h1>Your organisations</h1> <h1>Your organisations</h1>
{panels} {panels}
<h1>Add a new organisation</h1>
<CreateOrg></CreateOrg>
</>); </>);
} }

View file

@ -1,20 +1,23 @@
import {Breadcrumb, theme} from "antd"; import {Breadcrumb, Button, theme} from "antd";
import React from "react"; import React from "react";
//import {useOrgContext} from './OrgContext';
import CreateOrg from "./CreateOrg.tsx";
import {useUserContext} from "./hooks/UserContext.ts"; import {useUserContext} from "./hooks/UserContext.ts";
import {useNavigate} from "react-router";
import Organisations from "./Organisations.tsx";
const Home: React.FC = () => { const Home: React.FC = () => {
const { const {
token: { colorBgContainer, borderRadiusLG }, token: { colorBgContainer, borderRadiusLG },
} = theme.useToken(); } = theme.useToken();
const navigate = useNavigate();
const { currentUser } = useUserContext(); const { currentUser } = useUserContext();
console.log(currentUser.organisations);
if (currentUser.organisations.length === 0) { if (currentUser.organisations.length === 0) {
return <> return <>
<h3>Your user has no organisations yet. Create one here.</h3> <h2>Welcome to SR2 Cloud! </h2>
<CreateOrg/> <h3>If your organisation already uses SR2 Cloud, please ask your administrator to add you to the organisation.</h3>
<p> If your organanisation is new to SR2 Cloud, start the onboarding process </p>
<Button type="primary" onClick={() => navigate('/organisations')}> Start onboarding </Button>
</> </>
} }
@ -30,7 +33,9 @@ const Home: React.FC = () => {
}} }}
> >
<h1>Welcome to your dashboard, {currentUser.first_name}!</h1> <h1>Welcome to your dashboard, {currentUser.first_name}!</h1>
<div>
<Organisations/>
</div>
</div> </div>
</>) </>)
} }

View file

@ -10,8 +10,8 @@ export interface OrgObject {
status: "partial", status: "partial",
root_user_email: string, root_user_email: string,
billing_contact: Contact, billing_contact: Contact,
owner_contact: Conotact, owner_contact: Contact,
security_contact:Cntact, security_contact:Contact,
organisation_id: number, organisation_id: number,
intake_questionnaire: { intake_questionnaire: {
question_one: string, question_one: string,

View file

@ -15,7 +15,7 @@ const Profile = () => {
<> <>
<p>User: {currentUser.first_name} {currentUser.last_name}</p> <p>User: {currentUser.first_name} {currentUser.last_name}</p>
<p>Email: {currentUser.email}</p> <p>Email: {currentUser.email}</p>
<p>Current Orgs: {userOrgs[0].name}</p> {userOrgs[0] && <p>Current Orgs: {userOrgs[0].name}</p>}
</> </>
); );
}; };