Add an initial organisation flow
This commit is contained in:
parent
3bd1a5007a
commit
ac733af3b5
12 changed files with 432 additions and 99 deletions
|
|
@ -22,12 +22,14 @@
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/react-highlight-words": "^0.20.1",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"react-highlight-words": "^0.21.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12"
|
"vite": "^8.0.12"
|
||||||
|
|
|
||||||
69
src/App.tsx
69
src/App.tsx
|
|
@ -5,19 +5,20 @@ import React, {useState} from 'react';
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
DeploymentUnitOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {Breadcrumb, type MenuProps} from 'antd';
|
import {Breadcrumb, type MenuProps} from 'antd';
|
||||||
import { Layout, Menu, theme } from 'antd';
|
import { Layout, Menu, theme } from 'antd';
|
||||||
import Home from "./Home.tsx";
|
import Home from "./Home.tsx";
|
||||||
import {Route, Routes, useLocation, useNavigate} from "react-router";
|
import {Route, Routes, useLocation, useNavigate} from "react-router";
|
||||||
import Profile from "./Profile.tsx";
|
import Profile from "./Profile.tsx";
|
||||||
import {type UserContextType} from './UserContext.tsx';
|
import {type UserContextType} from './hooks/UserContext.ts';
|
||||||
import {UserContext} from './UserContext.tsx';
|
import {UserContext} from './hooks/UserContext.ts';
|
||||||
import type {userObject} from "./UserContext.tsx";
|
import type {userObject} from "./hooks/UserContext.ts";
|
||||||
|
|
||||||
import {type OrgContextType} from './OrgContext.tsx';
|
import Organisations from "./Organisations.tsx";
|
||||||
import {OrgContext} from './OrgContext.tsx';
|
import {RefreshContext} from "./hooks/RefreshContext.ts";
|
||||||
import type {orgObject} from "./OrgContext.tsx";
|
import Bridges from "./Bridges.tsx";
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
|
|
||||||
|
|
@ -66,28 +67,16 @@ const App: React.FC = () => {
|
||||||
first_name: "",
|
first_name: "",
|
||||||
last_name: "",
|
last_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
organisations: []
|
organisations: [],
|
||||||
|
id: -1
|
||||||
},
|
},
|
||||||
setCurrentUser: () => {}
|
setCurrentUser: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOrg: OrgContextType = {
|
|
||||||
currentOrg: {
|
|
||||||
billing_contact: "",
|
|
||||||
owner_contact: "",
|
|
||||||
name: "",
|
|
||||||
status: "",
|
|
||||||
root_user: "",
|
|
||||||
security_contact: "",
|
|
||||||
},
|
|
||||||
setCurrentOrg: () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser);
|
const [currentUser, setCurrentUser] = useState<userObject>(defaultUser.currentUser);
|
||||||
const [currentOrg, setCurrentOrg] = useState<orgObject>(defaultOrg.currentOrg);
|
const [refreshKey, setRefreshKey] = useState<number>(1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// STRILL NEEDS SOMETHING TO get CURRENT USER ORGS and map first one to a CURRENT ORG fetched from the API
|
|
||||||
// STILL NEEDS THE WRAPPER IN THE RETURN STATEMETNS
|
|
||||||
/*****************************
|
/*****************************
|
||||||
GETTING CURRENT USER FROM API
|
GETTING CURRENT USER FROM API
|
||||||
*****************************/
|
*****************************/
|
||||||
|
|
@ -108,7 +97,7 @@ const App: React.FC = () => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [auth]);
|
}, [auth, refreshKey]);
|
||||||
|
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
|
|
@ -142,8 +131,14 @@ const App: React.FC = () => {
|
||||||
case 'profile':
|
case 'profile':
|
||||||
navigate('/profile');
|
navigate('/profile');
|
||||||
break;
|
break;
|
||||||
|
case 'orgs':
|
||||||
|
navigate('/organisations');
|
||||||
|
break;
|
||||||
case 'logout':
|
case 'logout':
|
||||||
auth.removeUser().then(r => navigate('/profile'));
|
auth.removeUser().then(() => navigate('/profile'));
|
||||||
|
break;
|
||||||
|
case 'bridges':
|
||||||
|
navigate('/bridges');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Clicked item:', e.key);
|
console.log('Clicked item:', e.key);
|
||||||
|
|
@ -151,19 +146,26 @@ const App: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
//const organisations: [] = current_user.organisations;
|
let side_nav_items: MenuItem[] = []
|
||||||
|
|
||||||
const side_nav_items: MenuItem[] = currentUser.organisations && [
|
|
||||||
getItem('Files', '9', <SettingOutlined />),
|
|
||||||
];
|
|
||||||
|
|
||||||
const top_nav_items = [
|
const top_nav_items = [
|
||||||
getItem(currentUser.first_name + " " +currentUser.last_name , 'account', <UserOutlined />, [getItem('Account details', 'profile'), getItem('Log out', 'logout')]),
|
getItem(currentUser.first_name + " " +currentUser.last_name , 'account', <UserOutlined />, [getItem('Account details', 'profile'), getItem('Log out', 'logout')]),
|
||||||
getItem("Organisation Settings", 'orgsettings', <SettingOutlined />),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (currentUser.organisations.length > 0) {
|
||||||
|
|
||||||
|
side_nav_items = [ getItem('My organisations', 'orgs', <SettingOutlined />),
|
||||||
|
getItem('Bridges', 'bridges', <DeploymentUnitOutlined />), ];
|
||||||
|
|
||||||
|
const currentOrg = currentUser.organisations[0]['name']
|
||||||
|
top_nav_items.push(getItem(currentOrg +" Settings", 'orgsettings', <SettingOutlined />));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
|
<UserContext.Provider value={{ currentUser, setCurrentUser }}>
|
||||||
<Layout>
|
<RefreshContext.Provider value={{ refreshKey, setRefreshKey }}>
|
||||||
|
<Layout>
|
||||||
<Header style={{ display: 'flex', alignItems: 'right' }}>
|
<Header style={{ display: 'flex', alignItems: 'right' }}>
|
||||||
<div className="demo-logo" >
|
<div className="demo-logo" >
|
||||||
<h1>SR22</h1>
|
<h1>SR22</h1>
|
||||||
|
|
@ -206,12 +208,15 @@ const App: React.FC = () => {
|
||||||
<Routes>
|
<Routes>
|
||||||
<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="/bridges" element={<Bridges/>}/> }
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</RefreshContext.Provider>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
163
src/Bridges.tsx
Normal file
163
src/Bridges.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import type { InputRef, TableColumnsType, TableColumnType } from 'antd';
|
||||||
|
import { Button, Input, Space, Table } from 'antd';
|
||||||
|
import type { FilterDropdownProps } from 'antd/es/table/interface';
|
||||||
|
import Highlighter from 'react-highlight-words';
|
||||||
|
import { QRCode, theme } from 'antd';
|
||||||
|
const { useToken } = theme;
|
||||||
|
interface BridgeDataType {
|
||||||
|
fingerprint: string;
|
||||||
|
bridgeline: string;
|
||||||
|
qrCodeText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bridges = () => {
|
||||||
|
|
||||||
|
const bridgeLines: BridgeDataType[] = [
|
||||||
|
{fingerprint: '0C6DDE3F9FC377A5E69DC044E81C857277148D71', bridgeline: "obfs4 62.224.107.157:443 0C6DDE3F9FC377A5E69DC044E81C857277148D71 cert=6vp+cyZmJOp+QFtytv9Ca+Z+ASF4l7r12MeEevvufl4OcDimBhjQlxctjfgdCmnT7iu7Lg iat-mode=0", qrCodeText: "" },
|
||||||
|
{fingerprint:'8DFC222D2295A909B34B3AAAA584648EE6FAF14D', bridgeline: "obfs4 144.31.125.189:2063 8DFC222D2295A909B34B3AAAA584648EE6FAF14D cert=3BPE8q1dHF1AWoqsQEDGZHrRXPw/AaTwyM7YLGMOfFlxY6qRAlW1Jq0LlzKbe4Gh9+SacA iat-mode=0", qrCodeText: "" },
|
||||||
|
{fingerprint: '0C6DDE3F9FC377A5E69DC044E81C857277148D71', bridgeline: "obfs4 62.224.107.157:443 0C6DDE3F9FC377A5E69DC044E81C857277148D71 cert=6vp+cyZmJOp+QFtytv9Ca+Z+ASF4l7r12MeEevvufl4OcDimBhjQlxctjfgdCmnT7iu7Lg iat-mode=0", qrCodeText: ""},
|
||||||
|
{fingerprint:'8DFC222D2295A909B34B3AAAA584648EE6FAF14D', bridgeline: "obfs4 144.31.125.189:2063 8DFC222D2295A909B34B3AAAA584648EE6FAF14D cert=3BPE8q1dHF1AWoqsQEDGZHrRXPw/AaTwyM7YLGMOfFlxY6qRAlW1Jq0LlzKbe4Gh9+SacA iat-mode=0", qrCodeText: ""},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
function makeQRCodeText(bridgeline: string) {
|
||||||
|
return '["' + bridgeline + '"]';
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgeLines.forEach((line) => {line.qrCodeText = makeQRCodeText(line.bridgeline)});
|
||||||
|
console.log(bridgeLines);
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [searchedColumn, setSearchedColumn] = useState('');
|
||||||
|
const searchInput = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
const handleSearch = (
|
||||||
|
selectedKeys: string[],
|
||||||
|
confirm: FilterDropdownProps['confirm'],
|
||||||
|
dataIndex: DataIndex,
|
||||||
|
) => {
|
||||||
|
confirm();
|
||||||
|
setSearchText(selectedKeys[0]);
|
||||||
|
setSearchedColumn(dataIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = (clearFilters: () => void) => {
|
||||||
|
clearFilters();
|
||||||
|
setSearchText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataIndex = keyof BridgeDataType;
|
||||||
|
const getColumnSearchProps = (dataIndex: DataIndex): TableColumnType<BridgeDataType> => ({
|
||||||
|
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
|
||||||
|
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
|
||||||
|
<Input
|
||||||
|
ref={searchInput}
|
||||||
|
placeholder={`Search ${dataIndex}`}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
|
||||||
|
onPressEnter={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
|
||||||
|
style={{ marginBottom: 8, display: 'block' }}
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 90 }}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => clearFilters && handleReset(clearFilters)}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 90 }}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
confirm({ closeDropdown: false });
|
||||||
|
setSearchText((selectedKeys as string[])[0]);
|
||||||
|
setSearchedColumn(dataIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
filterIcon: (filtered: boolean) => (
|
||||||
|
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
|
||||||
|
),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record[dataIndex]
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes((value as string).toLowerCase()),
|
||||||
|
filterDropdownProps: {
|
||||||
|
onOpenChange(open) {
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => searchInput.current?.select(), 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (text) =>
|
||||||
|
searchedColumn === dataIndex ? (
|
||||||
|
<Highlighter
|
||||||
|
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
|
||||||
|
searchWords={[searchText]}
|
||||||
|
autoEscape
|
||||||
|
textToHighlight={text ? text.toString() : ''}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const bridgesColumns: TableColumnsType<BridgeDataType> = [
|
||||||
|
{title: 'Fingerprint',
|
||||||
|
dataIndex: 'fingerprint',
|
||||||
|
key: 'fingerprint',
|
||||||
|
width: '30%',
|
||||||
|
...getColumnSearchProps('fingerprint')},
|
||||||
|
|
||||||
|
{title: 'Bridgeline',
|
||||||
|
dataIndex: 'bridgeline',
|
||||||
|
key: 'bridgeline',
|
||||||
|
width: '40%',
|
||||||
|
...getColumnSearchProps('bridgeline')},
|
||||||
|
|
||||||
|
{title: 'QR Code',
|
||||||
|
width: '40%',
|
||||||
|
key: 'qrCodeText',
|
||||||
|
dataIndex: 'qrCodeText',
|
||||||
|
render: (text, record) => {return (<QRCode value={record.qrCodeText} />)}
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Table<BridgeDataType> columns={bridgesColumns} dataSource={bridgeLines} />;
|
||||||
|
</>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bridges;
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
// src/Posts.jsx
|
// src/Posts.jsx
|
||||||
|
|
||||||
import {useUserContext} from './UserContext';
|
import {useUserContext} from './hooks/UserContext.ts';
|
||||||
import { Button, Form, Input } from 'antd';
|
import { Button, Form, Input } from 'antd';
|
||||||
//import React, {useEffect, useState} from "react";
|
//import React, {useEffect, useState} from "react";
|
||||||
import {useAuth} from "react-oidc-context";
|
import {useAuth} from "react-oidc-context";
|
||||||
|
import {RefreshContext} from "./hooks/RefreshContext.ts";
|
||||||
|
import {useContext} from "react";
|
||||||
|
|
||||||
const CreateOrg = () => {
|
const CreateOrg = () => {
|
||||||
const { currentUser } = useUserContext();
|
const { currentUser } = useUserContext();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
const { refreshKey, setRefreshKey } = useContext(RefreshContext);
|
||||||
|
console.log(refreshKey);
|
||||||
async function submitOrgData(values: FormData) {
|
async function submitOrgData(values: FormData) {
|
||||||
if (!auth.user) {
|
if (!auth.user) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
console.log("in here");
|
|
||||||
const [name, q1, q2, q3] = Object.values(values);
|
const [name, q1, q2, q3] = Object.values(values);
|
||||||
console.log("end here");
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"intake_questionnaire": {
|
"intake_questionnaire": {
|
||||||
|
|
@ -28,16 +27,18 @@ const CreateOrg = () => {
|
||||||
"question_three": q3,
|
"question_three": q3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(data)
|
|
||||||
const token = auth.user.access_token;
|
const token = auth.user.access_token;
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {Authorization: `Bearer ${token}`,
|
headers: {Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json'}
|
'Content-Type': 'application/json'}
|
||||||
}
|
}
|
||||||
const response = await fetch("/api/v1/org/", requestOptions);
|
const response = await fetch("/api/v1/org", requestOptions);
|
||||||
await response.json();
|
await response.json();
|
||||||
|
setRefreshKey(prev => prev + 1);
|
||||||
|
console.log(refreshKey);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
@ -55,29 +56,29 @@ const CreateOrg = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form
|
<Form
|
||||||
layout= "vertical"
|
layout= "vertical"
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
>
|
>
|
||||||
<Form.Item name="Organisation name" label="Organisation name" rules={[{ required: true }]}>
|
<Form.Item name="Organisation name" label="Organisation name" rules={[{ required: true }]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="Question 1" label="How does your organisation support civil society?">
|
<Form.Item name="Question 1" label="How does your organisation support civil society?">
|
||||||
<Input placeholder="e.g., Our organisation supports various groups such as ..." />
|
<Input placeholder="e.g., Our organisation supports various groups such as ..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="Question 2" label="Name your first reference">
|
<Form.Item name="Question 2" label="Name your first reference">
|
||||||
<Input placeholder="e.g., SR2 Communciations Limited" />
|
<Input placeholder="e.g., SR2 Communciations Limited" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="Question 3" label="Name your second reference">
|
<Form.Item name="Question 3" label="Name your second reference">
|
||||||
<Input placeholder="e.g., SR2 Professional Services" />
|
<Input placeholder="e.g., SR2 Professional Services" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit">Submit</Button>
|
<Button type="primary" htmlType="submit">Submit</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import React, { createContext, useContext } from 'react';
|
|
||||||
|
|
||||||
interface orgObject {
|
|
||||||
name: string;
|
|
||||||
status: "partial",
|
|
||||||
root_user: "string",
|
|
||||||
owner_contact: "string",
|
|
||||||
billing_contact: "string",
|
|
||||||
security_contact: "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrgContextType {
|
|
||||||
currentOrg: orgObject;
|
|
||||||
setCurrentOrg: React.Dispatch<React.SetStateAction<orgObject>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export const useOrgContext = () => {
|
|
||||||
const context: OrgContextType | undefined = useContext(OrgContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('Organisation context not found');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
76
src/OrgPanel.tsx
Normal file
76
src/OrgPanel.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {Button, Card, Space} from 'antd';
|
||||||
|
import type {OrgObject} from "./hooks/OrgContext.ts";
|
||||||
|
import {useUserContext} from "./hooks/UserContext.ts";
|
||||||
|
import {useAuth} from "react-oidc-context";
|
||||||
|
import {useContext} from "react";
|
||||||
|
import {RefreshContext} from "./hooks/RefreshContext.ts";
|
||||||
|
|
||||||
|
const OrgPanel = (props: OrgObject) => {
|
||||||
|
|
||||||
|
const auth = useAuth();
|
||||||
|
const { refreshKey, setRefreshKey } = useContext(RefreshContext);
|
||||||
|
const {currentUser} = useUserContext();
|
||||||
|
async function removeUserFromOrg(org_id: number) {
|
||||||
|
if (!auth.user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const token = auth.user.access_token;
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'}
|
||||||
|
}
|
||||||
|
const response = await fetch("/api/v1/org/user?org_id=" + org_id + "&user_id=" + currentUser.id, requestOptions);
|
||||||
|
await response.json();
|
||||||
|
setRefreshKey(prev => prev + 1);
|
||||||
|
console.log(refreshKey)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function removeOrg(org_id: number) {
|
||||||
|
if (!auth.user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const token = auth.user.access_token;
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'}
|
||||||
|
}
|
||||||
|
await fetch("/api/v1/org/self?org_id=" + org_id, requestOptions);
|
||||||
|
setRefreshKey(prev => prev + 1);
|
||||||
|
console.log(refreshKey)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space vertical size={16}>
|
||||||
|
<Card title={props.name} extra={<a href="#">More</a>} style={{width: 300}}>
|
||||||
|
<p>Validated? {props.status}</p>
|
||||||
|
<p>Root User: {props.root_user_email}</p>
|
||||||
|
<p>Billing Contact: {props.billing_contact?.email || 'Not set'}</p>
|
||||||
|
<h3>Questionnaire</h3>
|
||||||
|
<p>Q1: {props.intake_questionnaire?.question_one || 'Not set'}</p>
|
||||||
|
<p>Q2: {props.intake_questionnaire?.question_two || 'Not set'}</p>
|
||||||
|
<p>Q3: {props.intake_questionnaire?.question_three || 'Not set'}</p>
|
||||||
|
|
||||||
|
<Button type="primary" onClick={() => removeUserFromOrg(props.organisation_id)}>
|
||||||
|
Remove yourself from organisation
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" danger onClick={() => removeOrg(props.organisation_id)}>
|
||||||
|
Remove organisation
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default OrgPanel;
|
||||||
51
src/Organisations.tsx
Normal file
51
src/Organisations.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import OrgPanel from "./OrgPanel";
|
||||||
|
import {useAuth} from "react-oidc-context";
|
||||||
|
import type {OrgObject} from "./hooks/OrgContext.ts";
|
||||||
|
import type { JSX } from "react/jsx-runtime";
|
||||||
|
import CreateOrg from "./CreateOrg.tsx";
|
||||||
|
import {useRefreshContext} from "./hooks/RefreshContext.ts";
|
||||||
|
|
||||||
|
const Organisations: React.FC = () => {
|
||||||
|
const auth = useAuth();
|
||||||
|
const [orgData, setCurrentOrgData] = useState<OrgObject[]>();
|
||||||
|
const { refreshKey } = useRefreshContext();
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchOrgData = async () => {
|
||||||
|
console.log("Fetching org data");
|
||||||
|
if (!auth.user) return
|
||||||
|
try {
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${auth.user.access_token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await fetch("/api/v1/user/self/orgs", requestOptions);
|
||||||
|
const orgsObject = await response.json();
|
||||||
|
setCurrentOrgData(orgsObject['organisations'])
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchOrgData()
|
||||||
|
.catch(console.error);
|
||||||
|
}, [auth.user, refreshKey]);
|
||||||
|
|
||||||
|
console.log(orgData);
|
||||||
|
|
||||||
|
let panels: JSX.Element[] = [];
|
||||||
|
if (orgData) {
|
||||||
|
panels = orgData.map(org => <OrgPanel {...org} key={org.organisation_id} />);
|
||||||
|
}
|
||||||
|
return(<>
|
||||||
|
<h1>Your organisations</h1>
|
||||||
|
{panels}
|
||||||
|
<h1>Add a new organisation</h1>
|
||||||
|
<CreateOrg></CreateOrg>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Organisations;
|
||||||
11
src/home.tsx
11
src/home.tsx
|
|
@ -1,15 +1,14 @@
|
||||||
import {Breadcrumb, theme} from "antd";
|
import {Breadcrumb, theme} from "antd";
|
||||||
import React, {useContext} from "react";
|
import React from "react";
|
||||||
//import {useOrgContext} from './OrgContext';
|
//import {useOrgContext} from './OrgContext';
|
||||||
import CreateOrg from "./CreateOrg.tsx";
|
import CreateOrg from "./CreateOrg.tsx";
|
||||||
import {useUserContext} from "./UserContext.tsx";
|
import {useUserContext} from "./hooks/UserContext.ts";
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
token: { colorBgContainer, borderRadiusLG },
|
token: { colorBgContainer, borderRadiusLG },
|
||||||
} = theme.useToken();
|
} = theme.useToken();
|
||||||
|
|
||||||
//const { currentOrg } = useOrgContext();
|
|
||||||
const { currentUser } = useUserContext();
|
const { currentUser } = useUserContext();
|
||||||
console.log(currentUser.organisations);
|
console.log(currentUser.organisations);
|
||||||
if (currentUser.organisations.length === 0) {
|
if (currentUser.organisations.length === 0) {
|
||||||
|
|
@ -19,8 +18,9 @@ const Home: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: fix this breadcrumb
|
||||||
return (<>
|
return (<>
|
||||||
<Breadcrumb style={{ margin: '16px 0' }} items={[{ title: 'User' }, { title: 'Bill' }]} />
|
<Breadcrumb style={{ margin: '16px 0' }} items={[{ title: 'User' }, { title: 'Dashboard' }]} />
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: 24,
|
padding: 24,
|
||||||
|
|
@ -29,7 +29,8 @@ const Home: React.FC = () => {
|
||||||
borderRadius: borderRadiusLG,
|
borderRadius: borderRadiusLG,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Bill is a cat.
|
<h1>Welcome to your dashboard, {currentUser.first_name}!</h1>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/hooks/OrgContext.ts
Normal file
36
src/hooks/OrgContext.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React, {createContext, useContext} from 'react';
|
||||||
|
|
||||||
|
export interface Contact {
|
||||||
|
email: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgObject {
|
||||||
|
name: string,
|
||||||
|
status: "partial",
|
||||||
|
root_user_email: string,
|
||||||
|
billing_contact: Contact,
|
||||||
|
owner_contact: Conotact,
|
||||||
|
security_contact:Cntact,
|
||||||
|
organisation_id: number,
|
||||||
|
intake_questionnaire: {
|
||||||
|
question_one: string,
|
||||||
|
question_two: string,
|
||||||
|
question_three: string
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgContextType {
|
||||||
|
currentOrg: OrgObject;
|
||||||
|
setCurrentOrg: React.Dispatch<React.SetStateAction<OrgObject>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useOrgContext = () => {
|
||||||
|
const context: OrgContextType | undefined = useContext(OrgContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('Organisation context not found');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
19
src/hooks/RefreshContext.ts
Normal file
19
src/hooks/RefreshContext.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React, {createContext, useContext} from 'react';
|
||||||
|
|
||||||
|
export interface RefreshType {
|
||||||
|
refreshKey: number;
|
||||||
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshContext = createContext<RefreshType>({
|
||||||
|
refreshKey: 1,
|
||||||
|
setRefreshKey: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useRefreshContext = () => {
|
||||||
|
const context: RefreshType | undefined = useContext(RefreshContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('Organisation refresh context not found');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -5,7 +5,8 @@ export interface userObject {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
organisations: string[];
|
organisations: { name: string, id: number }[];
|
||||||
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContextType {
|
export interface UserContextType {
|
||||||
|
|
@ -21,4 +22,6 @@ export const useUserContext = () => {
|
||||||
throw new Error('User context not found!');
|
throw new Error('User context not found!');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/Posts.jsx
|
// src/Posts.jsx
|
||||||
|
|
||||||
import {useUserContext} from './UserContext';
|
import { useUserContext} from './hooks/UserContext.ts';
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { currentUser } = useUserContext();
|
const { currentUser } = useUserContext();
|
||||||
|
|
@ -8,14 +8,15 @@ const Profile = () => {
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
console.log(currentUser);
|
||||||
|
console.log(currentUser.organisations);
|
||||||
|
const userOrgs: { name: string, id: number }[] = currentUser.organisations;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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>Orgs: {currentUser.organisations}</p>
|
<p>Current Orgs: {userOrgs[0].name}</p>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue