Refactoring du tableau de bord et ajout du panel pour les DADs
This commit is contained in:
parent
c0ee95234d
commit
ac41b301a9
@ -1,26 +1,17 @@
|
||||
import React from 'react';
|
||||
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<div className="column is-6">
|
||||
<DecisionSupportFilePanel />
|
||||
</div>
|
||||
<div className="column is-3">
|
||||
<WorkgroupsPanel />
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<h3 className="is-size-3 subtitle level-item">D.A.Ds</h3>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>TODO</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="column is-3">
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
|
44
client/src/components/HomePage/DecisionSupportFilePanel.tsx
Normal file
44
client/src/components/HomePage/DecisionSupportFilePanel.tsx
Normal file
@ -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 { useDecisions } from '../../gql/queries/decisions';
|
||||
|
||||
export function DecisionSupportFilePanel() {
|
||||
const { user } = useUserProfile();
|
||||
const { decisions } = useDecisions();
|
||||
|
||||
const tabs: TabDefinition[] = [
|
||||
{
|
||||
label: 'Mes dossiers en cours',
|
||||
itemFilter: (item: Item) => {
|
||||
const dsf = item as DecisionSupportFile;
|
||||
return dsf.status === DecisionSupportFileStatus.Opened && inWorkgroup(user, dsf.workgroup);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Ouverts',
|
||||
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Opened
|
||||
},
|
||||
{
|
||||
label: 'Clos',
|
||||
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Closed
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<ItemPanel
|
||||
className='is-link'
|
||||
title="Dossiers d'Aide à la Décision"
|
||||
newItemUrl="/decisions/new"
|
||||
items={decisions}
|
||||
tabs={tabs}
|
||||
itemIconClassName='fas fa-folder'
|
||||
itemKey={item => item.id}
|
||||
itemLabel={item => item.title}
|
||||
itemUrl={item => `/decisions/${item.id}`}
|
||||
/>
|
||||
);
|
||||
}
|
121
client/src/components/HomePage/ItemPanel.tsx
Normal file
121
client/src/components/HomePage/ItemPanel.tsx
Normal file
@ -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<ItemPanelProps> = (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 (
|
||||
<Link to={itemUrl(item, i)} key={`item-${itemKey(item, i)}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className={itemIconClassName} aria-hidden="true"></i>
|
||||
</span>
|
||||
{itemLabel(item, i)}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
const tabs = props.tabs || [];
|
||||
|
||||
return (
|
||||
<nav className={`panel ${className}`}>
|
||||
<div className="level panel-heading mb-0">
|
||||
<div className="level-left">
|
||||
<p className="level-item">{title}</p>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<Link to={newItemUrl} className="button level-item is-outlined is-info is-inverted">
|
||||
<i className="icon fa fa-plus"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-block">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input" type="text" placeholder="Filtrer..." />
|
||||
<span className="icon is-left">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="panel-tabs">
|
||||
{
|
||||
tabs.map((tab, i) => {
|
||||
return (
|
||||
<a key={`workgroup-tab-${i}`}
|
||||
onClick={selectTab.bind(null, i)}
|
||||
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||
{tab.label}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
{
|
||||
itemElements.length > 0 ?
|
||||
itemElements :
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<em>Aucun élément pour l'instant.</em>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
)
|
||||
};
|
@ -1,98 +1,45 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { Workgroup, inWorkgroup } 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 { 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 workgroupsQuery = useWorkgroupsQuery();
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ state, setState ] = useState({ selectedTab: 0 });
|
||||
const { workgroups } = useWorkgroups();
|
||||
const { user } = useUserProfile();
|
||||
|
||||
const isLoading = userProfileQuery.loading || workgroupsQuery.loading;
|
||||
const { userProfile } = (userProfileQuery.data || {});
|
||||
const { workgroups } = (workgroupsQuery.data || {});
|
||||
|
||||
const filterTabs = [
|
||||
const tabs = [
|
||||
{
|
||||
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 : ''));
|
||||
})
|
||||
itemFilter: (item: Item) => {
|
||||
const wg = item as Workgroup;
|
||||
return wg.closedAt === null && inWorkgroup(user, wg);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Ouverts",
|
||||
filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt)
|
||||
itemFilter: (item: Item) => !(item as Workgroup).closedAt
|
||||
},
|
||||
{
|
||||
label: "Clos",
|
||||
filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt)
|
||||
itemFilter: (item: Item) => !!(item as Workgroup).closedAt
|
||||
}
|
||||
];
|
||||
|
||||
const selectTab = (tabIndex: number) => {
|
||||
setState(state => ({ ...state, selectedTab: tabIndex }));
|
||||
};
|
||||
|
||||
let workgroupsItems = [];
|
||||
|
||||
workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => {
|
||||
return (
|
||||
<Link to={`/workgroups/${wg.id}`} key={`wg-${wg.id}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className="fas fa-users" aria-hidden="true"></i>
|
||||
</span>
|
||||
{wg.name}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className="panel is-info">
|
||||
<div className="level panel-heading mb-0">
|
||||
<div className="level-left">
|
||||
<p className="level-item">
|
||||
Groupes de travail
|
||||
</p>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<Link to="/workgroups/new" className="button level-item is-outlined is-info is-inverted">
|
||||
<i className="icon fa fa-plus"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="panel-block">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input" type="text" placeholder="Filtrer..." />
|
||||
<span className="icon is-left">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div> */}
|
||||
<WithLoader loading={isLoading}>
|
||||
<p className="panel-tabs">
|
||||
{
|
||||
filterTabs.map((tab, i) => {
|
||||
return (
|
||||
<a key={`workgroup-tab-${i}`}
|
||||
onClick={selectTab.bind(null, i)}
|
||||
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||
{tab.label}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
{
|
||||
workgroupsItems.length > 0 ?
|
||||
workgroupsItems :
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<em>Aucun groupe dans cet catégorie pour l'instant.</em>
|
||||
</a>
|
||||
}
|
||||
</WithLoader>
|
||||
</nav>
|
||||
)
|
||||
<ItemPanel
|
||||
className='is-info'
|
||||
title="Groupes de travail"
|
||||
newItemUrl="/workgroups/new"
|
||||
items={workgroups}
|
||||
tabs={tabs}
|
||||
itemIconClassName='fas fa-users'
|
||||
itemKey={item => item.id}
|
||||
itemLabel={item => item.name}
|
||||
itemUrl={item => `/workgroups/${item.id}`}
|
||||
/>
|
||||
);
|
||||
}
|
32
client/src/gql/queries/decisions.tsx
Normal file
32
client/src/gql/queries/decisions.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { DecisionSupportFile } from '../../types/decision';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useGraphQLData } from './helper';
|
||||
|
||||
const QUERY_DECISIONS = gql`
|
||||
query decisions($filter: DecisionFilter) {
|
||||
decisions(filter: $filter) {
|
||||
id,
|
||||
title,
|
||||
sections
|
||||
createdAt,
|
||||
closedAt,
|
||||
votedAt,
|
||||
workgroup {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function useDecisionsQuery(options = {}) {
|
||||
return useQuery(QUERY_DECISIONS, options);
|
||||
}
|
||||
|
||||
export function useDecisions() {
|
||||
const { data, loading, error } = useGraphQLData<DecisionSupportFile[]>(
|
||||
QUERY_DECISIONS, 'decicions', []
|
||||
);
|
||||
return { decisions: data, loading, error };
|
||||
}
|
11
client/src/gql/queries/helper.ts
Normal file
11
client/src/gql/queries/helper.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useQuery, DocumentNode } from "@apollo/client";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useGraphQLData<T>(q: DocumentNode, key: string, defaultValue: T) {
|
||||
const query = useQuery(q);
|
||||
const [ data, setData ] = useState<T>(defaultValue);
|
||||
useEffect(() => {
|
||||
setData(query.data ? query.data[key] as T : defaultValue);
|
||||
}, [query.loading, query.data]);
|
||||
return { data, loading: query.loading, error: query.error };
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { User } from '../../types/user';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useGraphQLData } from './helper';
|
||||
|
||||
const QUERY_USER_PROFILE = gql`
|
||||
query userProfile {
|
||||
@ -13,4 +16,11 @@ query userProfile {
|
||||
|
||||
export function useUserProfileQuery() {
|
||||
return useQuery(QUERY_USER_PROFILE);
|
||||
}
|
||||
|
||||
export function useUserProfile() {
|
||||
const { data, loading, error } = useGraphQLData<User>(
|
||||
QUERY_USER_PROFILE, 'userProfile', {id: '', email: ''}
|
||||
);
|
||||
return { user: data, loading, error };
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { useGraphQLData } from './helper';
|
||||
|
||||
const QUERY_WORKGROUP = gql`
|
||||
query workgroups($filter: WorkgroupsFilter) {
|
||||
@ -18,4 +20,11 @@ const QUERY_WORKGROUP = gql`
|
||||
|
||||
export function useWorkgroupsQuery(options = {}) {
|
||||
return useQuery(QUERY_WORKGROUP, options);
|
||||
}
|
||||
|
||||
export function useWorkgroups() {
|
||||
const { data, loading, error } = useGraphQLData<Workgroup[]>(
|
||||
QUERY_WORKGROUP, 'workgroups', []
|
||||
);
|
||||
return { workgroups: data, loading, error };
|
||||
}
|
23
client/src/types/decision.tsx
Normal file
23
client/src/types/decision.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Workgroup } from "./workgroup";
|
||||
|
||||
export enum DecisionSupportFileStatus {
|
||||
Opened = "opened",
|
||||
Voted = "voted",
|
||||
Closed = "closed",
|
||||
}
|
||||
|
||||
export interface DecisionSupportFileSection {
|
||||
id: string
|
||||
}
|
||||
|
||||
// aka Dossier d'aide à la décision
|
||||
export interface DecisionSupportFile {
|
||||
id: string
|
||||
title: string
|
||||
sections: DecisionSupportFileSection[]
|
||||
status: DecisionSupportFileStatus
|
||||
workgroup: Workgroup,
|
||||
createdAt: Date
|
||||
votedAt?: Date
|
||||
closedAt?: Date
|
||||
}
|
@ -1,9 +1,18 @@
|
||||
import { User } from "./user";
|
||||
|
||||
export interface Workgroup {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: Date
|
||||
closedAt: Date
|
||||
members: [User]
|
||||
members: User[]
|
||||
}
|
||||
|
||||
export function inWorkgroup(u: User, wg: Workgroup): boolean {
|
||||
for (let m, i = 0; (m = wg.members[i]); i++) {
|
||||
if(m.id === u.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
Loading…
Reference in New Issue
Block a user