Création/mise à jour basique d'un DAD #15
|
@ -1,26 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||||
|
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
return (
|
return (
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column">
|
<div className="column is-6">
|
||||||
|
<DecisionSupportFilePanel />
|
||||||
|
</div>
|
||||||
|
<div className="column is-3">
|
||||||
<WorkgroupsPanel />
|
<WorkgroupsPanel />
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column is-3">
|
||||||
<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="box">
|
<div className="box">
|
||||||
<div className="level">
|
<div className="level">
|
||||||
<div className="level-left">
|
<div className="level-left">
|
||||||
|
|
|
@ -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}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 React, { useEffect, useState } from 'react';
|
||||||
import { Workgroup } from '../../types/workgroup';
|
import { Workgroup, inWorkgroup } from '../../types/workgroup';
|
||||||
import { User } from '../../types/user';
|
import { User } from '../../types/user';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
import { useWorkgroupsQuery, useWorkgroups } from '../../gql/queries/workgroups';
|
||||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
import { useUserProfileQuery, useUserProfile } from '../../gql/queries/profile';
|
||||||
import { WithLoader } from '../WithLoader';
|
import { WithLoader } from '../WithLoader';
|
||||||
|
import { ItemPanel, Item } from './ItemPanel';
|
||||||
|
|
||||||
export function WorkgroupsPanel() {
|
export function WorkgroupsPanel() {
|
||||||
const workgroupsQuery = useWorkgroupsQuery();
|
const { workgroups } = useWorkgroups();
|
||||||
const userProfileQuery = useUserProfileQuery();
|
const { user } = useUserProfile();
|
||||||
const [ state, setState ] = useState({ selectedTab: 0 });
|
|
||||||
|
|
||||||
const isLoading = userProfileQuery.loading || workgroupsQuery.loading;
|
const tabs = [
|
||||||
const { userProfile } = (userProfileQuery.data || {});
|
|
||||||
const { workgroups } = (workgroupsQuery.data || {});
|
|
||||||
|
|
||||||
const filterTabs = [
|
|
||||||
{
|
{
|
||||||
label: "Mes groupes en cours",
|
label: "Mes groupes en cours",
|
||||||
filter: workgroups => workgroups.filter((wg: Workgroup) => {
|
itemFilter: (item: Item) => {
|
||||||
return wg.closedAt === null && wg.members.some((u: User) => u.id === (userProfile ? userProfile.id : ''));
|
const wg = item as Workgroup;
|
||||||
})
|
return wg.closedAt === null && inWorkgroup(user, wg);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Ouverts",
|
label: "Ouverts",
|
||||||
filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt)
|
itemFilter: (item: Item) => !(item as Workgroup).closedAt
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Clos",
|
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 (
|
return (
|
||||||
<nav className="panel is-info">
|
<ItemPanel
|
||||||
<div className="level panel-heading mb-0">
|
className='is-info'
|
||||||
<div className="level-left">
|
title="Groupes de travail"
|
||||||
<p className="level-item">
|
newItemUrl="/workgroups/new"
|
||||||
Groupes de travail
|
items={workgroups}
|
||||||
</p>
|
tabs={tabs}
|
||||||
</div>
|
itemIconClassName='fas fa-users'
|
||||||
<div className="level-right">
|
itemKey={item => item.id}
|
||||||
<Link to="/workgroups/new" className="button level-item is-outlined is-info is-inverted">
|
itemLabel={item => item.name}
|
||||||
<i className="icon fa fa-plus"></i>
|
itemUrl={item => `/workgroups/${item.id}`}
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
}
|
|
@ -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 };
|
||||||
|
}
|
|
@ -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 { gql, useQuery } from '@apollo/client';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
const QUERY_USER_PROFILE = gql`
|
const QUERY_USER_PROFILE = gql`
|
||||||
query userProfile {
|
query userProfile {
|
||||||
|
@ -13,4 +16,11 @@ query userProfile {
|
||||||
|
|
||||||
export function useUserProfileQuery() {
|
export function useUserProfileQuery() {
|
||||||
return useQuery(QUERY_USER_PROFILE);
|
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 { gql, useQuery } from '@apollo/client';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
const QUERY_WORKGROUP = gql`
|
const QUERY_WORKGROUP = gql`
|
||||||
query workgroups($filter: WorkgroupsFilter) {
|
query workgroups($filter: WorkgroupsFilter) {
|
||||||
|
@ -18,4 +20,11 @@ const QUERY_WORKGROUP = gql`
|
||||||
|
|
||||||
export function useWorkgroupsQuery(options = {}) {
|
export function useWorkgroupsQuery(options = {}) {
|
||||||
return useQuery(QUERY_WORKGROUP, options);
|
return useQuery(QUERY_WORKGROUP, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkgroups() {
|
||||||
|
const { data, loading, error } = useGraphQLData<Workgroup[]>(
|
||||||
|
QUERY_WORKGROUP, 'workgroups', []
|
||||||
|
);
|
||||||
|
return { workgroups: data, loading, error };
|
||||||
}
|
}
|
|
@ -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";
|
import { User } from "./user";
|
||||||
|
|
||||||
export interface Workgroup {
|
export interface Workgroup {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
closedAt: 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