Refactoring du tableau de bord et ajout du panel pour les DADs

This commit is contained in:
wpetit 2020-07-31 17:36:10 +02:00
parent c0ee95234d
commit ac41b301a9
10 changed files with 292 additions and 95 deletions

View File

@ -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">

View 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}`}
/>
);
}

View 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>
)
};

View File

@ -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}`}
/>
);
}

View 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 };
}

View 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 };
}

View File

@ -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 };
}

View File

@ -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 };
}

View 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
}

View File

@ -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;
}