diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx index 89e3dca..f0b3e67 100644 --- a/client/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -1,20 +1,49 @@ -import React from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { HomePage } from './HomePage/HomePage'; import { ProfilePage } from './ProfilePage/ProfilePage'; import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage'; +import { DecisionSupportFilePage } from './DecisionSupportFilePage/DecisionSupportFilePage'; +import { DashboardPage } from './DashboardPage/DashboardPage'; +import { useUserProfile } from '../gql/queries/profile'; +import { LoggedInContext } from '../hooks/useLoggedIn'; +import { PrivateRoute } from './PrivateRoute'; +import { useKonamiCode } from '../hooks/useKonamiCode'; +import { Modal } from './Modal'; -export class App extends React.Component { - render() { - return ( +export interface AppProps { + +} + +export const App: FunctionComponent = () => { + const { user } = useUserProfile(); + + const [ showBoneyM, setShowBoneyM ] = useState(false); + useKonamiCode(() => setShowBoneyM(true)); + + return ( + - - + + + + } /> - ); - } + { + showBoneyM ? + setShowBoneyM(false)}> + + : + null + } + + ); } \ No newline at end of file diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/DashboardPage/Dashboard.tsx similarity index 56% rename from client/src/components/HomePage/Dashboard.tsx rename to client/src/components/DashboardPage/Dashboard.tsx index 916fba1..967cac2 100644 --- a/client/src/components/HomePage/Dashboard.tsx +++ b/client/src/components/DashboardPage/Dashboard.tsx @@ -1,26 +1,17 @@ import React from 'react'; import { WorkgroupsPanel } from './WorkgroupsPanel'; +import { DecisionSupportFilePanel } from './DecisionSupportFilePanel'; export function Dashboard() { return (
-
+
+ +
+
-
-
-
-
-

D.A.Ds

-
-
- -
-
-
TODO
-
-
-
+
diff --git a/client/src/components/DashboardPage/DashboardPage.tsx b/client/src/components/DashboardPage/DashboardPage.tsx new file mode 100644 index 0000000..55726c6 --- /dev/null +++ b/client/src/components/DashboardPage/DashboardPage.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Page } from '../Page'; +import { Dashboard } from './Dashboard'; + +export function DashboardPage() { + return ( + +
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/DashboardPage/DecisionSupportFilePanel.tsx b/client/src/components/DashboardPage/DecisionSupportFilePanel.tsx new file mode 100644 index 0000000..1fa31e8 --- /dev/null +++ b/client/src/components/DashboardPage/DecisionSupportFilePanel.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision'; +import { ItemPanel, TabDefinition, Item } from './ItemPanel'; +import { useUserProfile } from '../../gql/queries/profile'; +import { inWorkgroup } from '../../types/workgroup'; +import { useDecisionSupportFiles } from '../../gql/queries/dsf'; + +export function DecisionSupportFilePanel() { + const { user } = useUserProfile(); + const { decisionSupportFiles } = useDecisionSupportFiles(); + + const tabs: TabDefinition[] = [ + { + label: 'Mes dossiers en cours', + itemFilter: (item: Item) => { + const dsf = item as DecisionSupportFile; + return (dsf.status === DecisionSupportFileStatus.Draft || dsf.status === DecisionSupportFileStatus.Ready) && inWorkgroup(user, dsf.workgroup); + } + }, + { + label: 'Brouillons', + itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Draft + }, + { + label: 'Clos', + itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Closed + }, + ]; + + + return ( + item.id} + itemLabel={item => item.title} + itemUrl={item => `/decisions/${item.id}`} + /> + ); +} \ No newline at end of file diff --git a/client/src/components/DashboardPage/ItemPanel.tsx b/client/src/components/DashboardPage/ItemPanel.tsx new file mode 100644 index 0000000..4335c68 --- /dev/null +++ b/client/src/components/DashboardPage/ItemPanel.tsx @@ -0,0 +1,121 @@ +import React, { FunctionComponent, useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { WithLoader } from "../WithLoader"; + +export interface Item { + id: string + [propName: string]: any; +} + +export interface TabDefinition { + label: string + itemFilter?: (item: Item) => boolean +} + + +export interface ItemPanelProps { + className?: string + itemIconClassName?: string + title?: string + newItemUrl: string + isLoading?: boolean + items: Item[] + tabs?: TabDefinition[], + itemKey: (item: Item, index: number) => string + itemLabel: (item: Item, index: number) => string + itemUrl: (item: Item, index: number) => string +} + +export const ItemPanel: FunctionComponent = (props) => { + const { + title, className, newItemUrl, + itemKey, itemLabel, + itemIconClassName, itemUrl + } = props; + + const [ state, setState ] = useState({ selectedTab: 0, filteredItems: [] }); + + const filterItemsForTab = (tab: TabDefinition, items: Item[]) => { + const itemFilter = tab && typeof tab.itemFilter === 'function' ? tab.itemFilter : () => true; + return items.filter(itemFilter); + }; + + const selectTab = (tabIndex: number) => { + setState(state => { + const { tabs, items } = props; + const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[tabIndex] : null; + return { + ...state, + selectedTab: tabIndex, + filteredItems: filterItemsForTab(newTab, items) + }; + }); + }; + + useEffect(() => { + setState(state => { + const { tabs, items } = props; + const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[state.selectedTab] : null; + return { + ...state, + filteredItems: filterItemsForTab(newTab, items), + } + }); + }, [props.items, props.tabs]); + + const itemElements = state.filteredItems.map((item: Item, i: number) => { + return ( + + + + + {itemLabel(item, i)} + + ); + }); + + const tabs = props.tabs || []; + + return ( + + ) +}; \ No newline at end of file diff --git a/client/src/components/DashboardPage/WorkgroupsPanel.tsx b/client/src/components/DashboardPage/WorkgroupsPanel.tsx new file mode 100644 index 0000000..c7ea0d1 --- /dev/null +++ b/client/src/components/DashboardPage/WorkgroupsPanel.tsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from 'react'; +import { Workgroup, inWorkgroup } from '../../types/workgroup'; +import { User } from '../../types/user'; +import { Link } from 'react-router-dom'; +import { useWorkgroupsQuery, useWorkgroups } from '../../gql/queries/workgroups'; +import { useUserProfileQuery, useUserProfile } from '../../gql/queries/profile'; +import { WithLoader } from '../WithLoader'; +import { ItemPanel, Item } from './ItemPanel'; + +export function WorkgroupsPanel() { + const { workgroups } = useWorkgroups(); + const { user } = useUserProfile(); + + const tabs = [ + { + label: "Mes groupes en cours", + itemFilter: (item: Item) => { + const wg = item as Workgroup; + return wg.closedAt === null && inWorkgroup(user, wg); + } + }, + { + label: "Ouverts", + itemFilter: (item: Item) => !(item as Workgroup).closedAt + }, + { + label: "Clos", + itemFilter: (item: Item) => !!(item as Workgroup).closedAt + } + ]; + + return ( + item.id} + itemLabel={item => item.name} + itemUrl={item => `/workgroups/${item.id}`} + /> + ); +} \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/AppendixPanel.tsx b/client/src/components/DecisionSupportFilePage/AppendixPanel.tsx new file mode 100644 index 0000000..97603a4 --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/AppendixPanel.tsx @@ -0,0 +1,18 @@ +import React, { FunctionComponent, useState } from 'react'; +import { DecisionSupportFile } from '../../types/decision'; + +export interface AppendixPanelProps { + dsf: DecisionSupportFile, +}; + +export const AppendixPanel: FunctionComponent = ({ dsf }) => { + return ( + + ); +}; \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/ClarificationSection.tsx b/client/src/components/DecisionSupportFilePage/ClarificationSection.tsx new file mode 100644 index 0000000..264dfbc --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/ClarificationSection.tsx @@ -0,0 +1,134 @@ +import React, { FunctionComponent, useState, ChangeEvent, useEffect } from 'react'; +import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps'; +import { useDebounce } from '../../hooks/useDebounce'; +import { asDate } from '../../util/date'; + +export interface ClarificationSectionProps extends DecisionSupportFileUpdaterProps {}; + +const ClarificationSectionName = 'clarification'; + +export const ClarificationSection: FunctionComponent = ({ dsf, updateDSF }) => { + const [ state, setState ] = useState({ + changed: false, + section: { + objectives: '', + motivations: '', + scope: '', + nature: '', + deadline: undefined, + hasDeadline: false, + } + }); + + useEffect(() => { + if (!state.changed) return; + updateDSF({ ...dsf, sections: { ...dsf.sections, [ClarificationSectionName]: { ...state.section }} }) + setState(state => ({ ...state, changed: false })); + }, [state.changed]); + + useEffect(() => { + if (!dsf.sections[ClarificationSectionName]) return; + setState(state => ({ ...state, changed: false, section: {...state.section, ...dsf.sections[ClarificationSectionName] }})); + }, [dsf.sections[ClarificationSectionName]]); + + const onTitleChange = (evt: ChangeEvent) => { + const title = (evt.currentTarget).value; + updateDSF({ ...dsf, title }); + }; + + const onSectionAttrChange = (attrName: string, evt: ChangeEvent) => { + const target = evt.currentTarget; + const value = target.hasOwnProperty('checked') ? target.checked : target.value; + setState(state => ({ ...state, changed: true, section: {...state.section, [attrName]: value }})); + }; + + const onDeadlineChange = (evt: ChangeEvent) => { + const deadline = evt.currentTarget.valueAsDate; + setState(state => ({ ...state, changed: true, section: { ...state.section, deadline }})); + }; + + return ( +
+
+
+ +
+ +
+
+
+ +
+ +
+

Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.

+
+
+ +
+ +
+

Penser à indiquer si des obligations légales pèsent sur cette prise de décision.

+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/DecisionSupportFilePage.tsx b/client/src/components/DecisionSupportFilePage/DecisionSupportFilePage.tsx new file mode 100644 index 0000000..3af2a01 --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/DecisionSupportFilePage.tsx @@ -0,0 +1,155 @@ +import React, { FunctionComponent, useState, useEffect } from 'react'; +import { Page } from '../Page'; +import { ClarificationSection } from './ClarificationSection'; +import { MetadataPanel } from './MetadataPanel'; +import { AppendixPanel } from './AppendixPanel'; +import { DecisionSupportFile, newDecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision'; +import { useParams, useHistory } from 'react-router'; +import { useDecisionSupportFiles } from '../../gql/queries/dsf'; +import { useCreateDecisionSupportFileMutation, useUpdateDecisionSupportFileMutation } from '../../gql/mutations/dsf'; +import { useDebounce } from '../../hooks/useDebounce'; + +export interface DecisionSupportFilePageProps { + +}; + +export const DecisionSupportFilePage: FunctionComponent = () => { + const { id } = useParams(); + const history = useHistory(); + const { decisionSupportFiles } = useDecisionSupportFiles({ + variables:{ + filter: { + ids: id !== 'new' ? [id] : undefined, + } + } + }); + + const [ state, setState ] = useState({ + dsf: newDecisionSupportFile(), + saved: true, + selectedTabIndex: 0, + }); + + useEffect(() => { + const dsf = decisionSupportFiles.length > 0 && decisionSupportFiles[0].id === id ? decisionSupportFiles[0] : {}; + setState(state => ({ ...state, dsf: { ...state.dsf, ...dsf }})) + }, [ decisionSupportFiles ]); + + const selectTab = (tabIndex: number) => { + setState(state => ({ ...state, selectedTabIndex: tabIndex })); + }; + + const updateDSF = (dsf: DecisionSupportFile) => { + setState(state => { + return { ...state, saved: false, dsf: { ...state.dsf, ...dsf } }; + }); + }; + + const [ createDecisionSupportFile ] = useCreateDecisionSupportFileMutation(); + const [ updateDecisionSupportFile ] = useUpdateDecisionSupportFileMutation(); + + const saveDSF = () => { + const changes = { + title: state.dsf.title !== '' ? state.dsf.title : undefined, + status: state.dsf.status, + workgroupId: state.dsf.workgroup ? state.dsf.workgroup.id : undefined, + sections: state.dsf.sections, + }; + + if (!changes.workgroupId) return; + + if (state.dsf.id === '') { + createDecisionSupportFile({ + variables: { changes }, + }).then(({ data }) => { + history.push(`/decisions/${data.createDecisionSupportFile.id}`); + }); + } else { + updateDecisionSupportFile({ + variables: { changes, id: state.dsf.id }, + }).then(({ data }) => { + setState(state => { + return { ...state, saved: true, dsf: { ...state.dsf, ...data.updateDecisionSupportFile } }; + }); + }); + } + }; + + const canSave = !!state.dsf.workgroup && !state.saved; + const isNew = state.dsf.id === ''; + const isClosed = state.dsf.status === DecisionSupportFileStatus.Closed; + + return ( + +
+
+
+
+ { + isNew ? +
+
+

Nouveau

+

Dossier d'Aide à la Décision

+
+
: +
+
+

{state.dsf.title}

+

Dossier d'Aide à la Décision { isClosed ? '(clos)' : null }

+
+
+ } +
+
+
+ +
+
+
+
+
+
+ + { + state.selectedTabIndex === 0 ? + : + null + } +
+
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/DecisionSupportFileUpdaterProps.tsx b/client/src/components/DecisionSupportFilePage/DecisionSupportFileUpdaterProps.tsx new file mode 100644 index 0000000..5c065f9 --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/DecisionSupportFileUpdaterProps.tsx @@ -0,0 +1,6 @@ +import { DecisionSupportFile } from "../../types/decision"; + +export interface DecisionSupportFileUpdaterProps { + dsf: DecisionSupportFile + updateDSF: (dsf: DecisionSupportFile) => void +} \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/MetadataPanel.tsx b/client/src/components/DecisionSupportFilePage/MetadataPanel.tsx new file mode 100644 index 0000000..9e4e823 --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/MetadataPanel.tsx @@ -0,0 +1,86 @@ +import React, { FunctionComponent, useState, useEffect, ChangeEvent } from 'react'; +import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision'; +import { useWorkgroups } from '../../gql/queries/workgroups'; +import { useUserProfile } from '../../gql/queries/profile'; +import { inWorkgroup } from '../../types/workgroup'; +import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps'; +import { asDate } from '../../util/date'; + +export interface MetadataPanelProps extends DecisionSupportFileUpdaterProps {}; + +export const MetadataPanel: FunctionComponent = ({ dsf, updateDSF }) => { + const { user } = useUserProfile(); + const { workgroups } = useWorkgroups(); + + const [ userOpenedWorkgroups, setUserOpenedWorkgroups ] = useState([]); + + useEffect(() => { + const filtered = workgroups.filter(wg => !wg.closedAt && inWorkgroup(user, wg)); + setUserOpenedWorkgroups(filtered); + }, [workgroups, user]) + + const onStatusChanged = (evt: ChangeEvent) => { + const status = evt.currentTarget.value as DecisionSupportFileStatus; + updateDSF({ ...dsf, status }); + }; + + const onWorkgroupChanged = (evt: ChangeEvent) => { + const workgroupId = evt.currentTarget.value; + const workgroup = workgroups.find(wg => wg.id === workgroupId); + updateDSF({ ...dsf, workgroup }); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/client/src/components/DecisionSupportFilePage/OptionsSection.tsx b/client/src/components/DecisionSupportFilePage/OptionsSection.tsx new file mode 100644 index 0000000..bd537cd --- /dev/null +++ b/client/src/components/DecisionSupportFilePage/OptionsSection.tsx @@ -0,0 +1,17 @@ +import React, { FunctionComponent, useState } from 'react'; +import { DecisionSupportFile } from '../../types/decision'; + +export interface OptionsSectionProps { + dsf: DecisionSupportFile, +}; + +export const OptionsSection: FunctionComponent = ({ dsf }) => { + return ( +
+

Explorer les options

+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index 2e8e26a..f455d4e 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -1,33 +1,20 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Page } from '../Page'; -import { Dashboard } from './Dashboard'; -import { useUserProfileQuery } from '../../gql/queries/profile'; -import { WithLoader } from '../WithLoader'; +import { WelcomeContent } from './WelcomeContent'; +import { useUserProfile } from '../../gql/queries/profile'; +import { useHistory } from 'react-router'; export function HomePage() { - const { data, loading } = useUserProfileQuery(); + const { user } = useUserProfile(); + const history = useHistory(); - const { userProfile } = (data || {}); + useEffect(() => { + if (user.id !== '') history.push('/dashboard'); + }, [user.id]) return ( - -
-
- - { - userProfile ? - : -
-
-
-

Veuillez vous authentifier.

-
-
-
- } -
-
-
+ + ); } \ No newline at end of file diff --git a/client/src/components/HomePage/WelcomeContent.tsx b/client/src/components/HomePage/WelcomeContent.tsx new file mode 100644 index 0000000..03bd603 --- /dev/null +++ b/client/src/components/HomePage/WelcomeContent.tsx @@ -0,0 +1,75 @@ +import React, { FunctionComponent, Fragment } from "react"; + +export interface WelcomeContentProps { + +} + +export const WelcomeContent: FunctionComponent = () => { + return ( + +
+
+
+

+ Bienvenue sur Daddy ! +

+

+ L'outil de suivi de la vie d'entreprise démocratique. +

+
+
+
+
+

+ Attention Le service est actuellement en alpha. L'accès est restreint aux adresses autorisées. +

+
+
+
+
+
+
+ +
+
+
+

Une adresse courriel et c'est parti !

+

Pas de création de compte, pas de mot de passe à retenir. Entrez votre adresse courriel et commencez directement à travailler !

+ {/*

En savoir plus

*/} +
+
+
+
+
+
+
+ +
+
+
+

Préparer vos dossiers d'aide à la décision

+

Une décision à prendre ? Un nouveau projet à lancer ? Crééz votre groupe de travail et rédigez un dossier pour faciliter la prise de décision collective !

+ {/*

En savoir plus

*/} +
+
+
+
+
+
+
+ +
+
+
+

Travaillez collaborativement

+

Éditez à plusieurs vos dossiers d'aide à la décision, suivi l'avancée des débats et retrouvez simplement les décisions prises dans l'entreprise.

+ {/*

En savoir plus

*/} +
+
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx deleted file mode 100644 index 439b931..0000000 --- a/client/src/components/HomePage/WorkgroupsPanel.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Workgroup } from '../../types/workgroup'; -import { User } from '../../types/user'; -import { Link } from 'react-router-dom'; -import { useWorkgroupsQuery } from '../../gql/queries/workgroups'; -import { useUserProfileQuery } from '../../gql/queries/profile'; -import { WithLoader } from '../WithLoader'; - -export function WorkgroupsPanel() { - const workgroupsQuery = useWorkgroupsQuery(); - const userProfileQuery = useUserProfileQuery(); - const [ state, setState ] = useState({ selectedTab: 0 }); - - const isLoading = userProfileQuery.loading || workgroupsQuery.loading; - const { userProfile } = (userProfileQuery.data || {}); - const { workgroups } = (workgroupsQuery.data || {}); - - const filterTabs = [ - { - label: "Mes groupes en cours", - filter: workgroups => workgroups.filter((wg: Workgroup) => { - return wg.closedAt === null && wg.members.some((u: User) => u.id === (userProfile ? userProfile.id : '')); - }) - }, - { - label: "Ouverts", - filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt) - }, - { - label: "Clos", - filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt) - } - ]; - - const selectTab = (tabIndex: number) => { - setState(state => ({ ...state, selectedTab: tabIndex })); - }; - - let workgroupsItems = []; - - workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => { - return ( - - - - - {wg.name} - - ); - }); - - return ( - - ) -} \ No newline at end of file diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index 0bd718f..a5d38fe 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -3,11 +3,10 @@ import logo from '../resources/logo.svg'; import { useSelector } from 'react-redux'; import { Config } from '../config'; import { Link } from 'react-router-dom'; -import { useUserProfileQuery } from '../gql/queries/profile'; -import { WithLoader } from './WithLoader'; +import { useLoggedIn } from '../hooks/useLoggedIn'; export function Navbar() { - const userProfileQuery = useUserProfileQuery(); + const loggedIn = useLoggedIn(); const [ isActive, setActive ] = useState(false); const toggleMenu = () => { @@ -15,7 +14,7 @@ export function Navbar() { }; return ( -