has been added. It may be break your style.',
78 | );
79 | }
80 |
81 | render() {
82 | return (
83 |
87 | );
88 | }
89 | }
90 |
91 | export default PreventCrash;
92 |
--------------------------------------------------------------------------------
/src/components/UnitDisplay/index.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import React from 'react';
3 |
4 | import fbt from '../../fbt/fbt.macro';
5 | import useUserInfos from '../../utils/useUserInfos';
6 |
7 | const UnitDisplay = ({unit}) => {
8 | const {workingTime = 8} = useUserInfos();
9 |
10 | if (unit === 0) {
11 | return '—';
12 | }
13 |
14 | let unitInHours = false;
15 |
16 | let unitToDisplay = unit;
17 |
18 | if (unit < 1) {
19 | unitToDisplay *= workingTime;
20 | unitInHours = true;
21 | }
22 |
23 | return unitInHours
24 | ? moment.duration(unitToDisplay, 'hours').humanize()
25 | : moment.duration(unitToDisplay, 'days').humanize();
26 | };
27 |
28 | export const UnitWorkedDisplay = props => (
29 | <>
30 |
{' '}
31 | {props.unit < 1 ? (
32 |
33 |
38 | travaillée
39 |
40 |
41 | ) : (
42 |
43 |
48 | travaillé
49 |
50 |
51 | )}
52 | >
53 | );
54 |
55 | export const UnitAvailableDisplay = props => (
56 | <>
57 |
{' '}
58 |
59 |
64 | encore disponible
65 |
66 |
67 | >
68 | );
69 |
70 | export const UnitOvertimeDisplay = props => (
71 | <>
72 |
{' '}
73 |
74 |
79 | supplémentaire
80 |
81 |
82 | >
83 | );
84 |
85 | export default UnitDisplay;
86 |
--------------------------------------------------------------------------------
/src/components/UploadDashboardButton/index.js:
--------------------------------------------------------------------------------
1 | import '@uppy/core/dist/style.css';
2 | import '@uppy/dashboard/dist/style.css';
3 |
4 | import Portal from '@reach/portal';
5 | import Uppy, {Plugin} from '@uppy/core';
6 | import uppyFrenchLocale from '@uppy/locales/lib/fr_FR';
7 | import DashboardModal from '@uppy/react/lib/DashboardModal';
8 | import {IntlViewerContext} from 'fbt';
9 | import React, {useEffect, useMemo, useState} from 'react';
10 |
11 | import fbt from '../../fbt/fbt.macro';
12 | import {Button} from '../../utils/new/design-system';
13 |
14 | class Upload extends Plugin {
15 | constructor(uppy, opts) {
16 | super(uppy, opts);
17 | this.id = opts.id || 'graphqlupload';
18 | this.type = 'GraphQLUpload';
19 |
20 | this.callback = opts.callback;
21 | }
22 |
23 | install() {
24 | this.uppy.addUploader((fileIDs) => {
25 | const files = fileIDs.map(fileID => this.uppy.getFile(fileID).data);
26 |
27 | return this.callback(files);
28 | });
29 | }
30 | }
31 |
32 | function UploadDashboardButton({
33 | onUploadFiles,
34 | children,
35 | allowMultipleUploads,
36 | restrictions,
37 | autoProceed,
38 | style,
39 | }) {
40 | const [modalOpen, setModalOpen] = useState(false);
41 | const uppyState = useMemo(
42 | () => Uppy({
43 | autoProceed,
44 | allowMultipleUploads,
45 | restrictions,
46 | locale:
47 | IntlViewerContext.locale.startsWith('fr')
48 | && uppyFrenchLocale,
49 | }).use(Upload, {
50 | callback: onUploadFiles,
51 | }),
52 | [autoProceed, allowMultipleUploads, restrictions, onUploadFiles],
53 | );
54 |
55 | useEffect(() => {
56 | uppyState.on('complete', () => {
57 | if (!allowMultipleUploads) {
58 | uppyState.reset();
59 | }
60 | });
61 |
62 | return () => uppyState.close();
63 | }, [uppyState]);
64 |
65 | return (
66 | <>
67 |
setModalOpen(true)}
71 | style={style}
72 | >
73 | {children}
74 |
75 |
76 | setModalOpen(false)}
79 | closeModalOnClickOutside
80 | closeAfterFinish={true}
81 | uppy={uppyState}
82 | note={
83 |
84 | 5Mo total maximum
85 |
86 | }
87 | />
88 |
89 | >
90 | );
91 | }
92 |
93 | UploadDashboardButton.defaultProps = {
94 | onUploadFiles: () => {},
95 | };
96 |
97 | export default UploadDashboardButton;
98 |
--------------------------------------------------------------------------------
/src/components/WelcomeModal/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React from 'react';
3 | import YouTube from 'react-youtube';
4 |
5 | import fbt from '../../fbt/fbt.macro';
6 | import {ModalActions, ModalContainer, ModalElem} from '../../utils/content';
7 | import {Button, Heading, P} from '../../utils/new/design-system';
8 |
9 | const PA = styled(P)`
10 | font-size: 16px;
11 | `;
12 |
13 | const IframeYouTube = styled(YouTube)`
14 | position: absolute;
15 | width: 100%;
16 | height: 100%;
17 | `;
18 |
19 | const YoutubeContainer = styled('div')`
20 | position: relative;
21 | overflow: hidden;
22 | width: 100%;
23 | height: 0;
24 | padding-bottom: 56.25%;
25 | `;
26 |
27 | const WelcomeModal = ({onDismiss}) => (
28 |
29 |
30 |
31 | Bienvenue sur Inyo,
32 |
33 | Découvrez en 1'30 min les options de bases de Inyo et
34 | commencez dès maintenant à optimiser vos journées!
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | J'ai compris!
44 |
45 |
46 |
47 |
48 |
49 | );
50 |
51 | export default WelcomeModal;
52 |
--------------------------------------------------------------------------------
/src/fbt/fbt.macro.js:
--------------------------------------------------------------------------------
1 | const {createMacro} = require('babel-plugin-macros');
2 | const babelPluginFbt = require('babel-plugin-fbt');
3 | const babelPluginFbtRuntime = require('babel-plugin-fbt-runtime');
4 | const {addDefault} = require('@babel/helper-module-imports');
5 |
6 | module.exports = createMacro(fbt);
7 |
8 | function fbt({references, state, babel}) {
9 | const {default: defaultImport = []} = references;
10 | const instanceBPF = babelPluginFbt(babel);
11 | const instanceBPFRuntime = babelPluginFbtRuntime(babel);
12 |
13 | const importName = addDefault(state.file.path, 'fbt', {
14 | ensureNoContext: true,
15 | nameHint: 'fbt',
16 | });
17 |
18 | Object.assign(instanceBPF, state);
19 | Object.assign(instanceBPFRuntime, state);
20 |
21 | instanceBPF.pre();
22 | instanceBPFRuntime.pre();
23 |
24 | state.file.path.traverse(instanceBPF.visitor, state);
25 | state.file.path.traverse(instanceBPFRuntime.visitor, state);
26 |
27 | state.file.path.traverse(
28 | {
29 | CallExpression(path) {
30 | if (
31 | path.node.callee.type === 'MemberExpression'
32 | && path.node.callee.object
33 | && path.node.callee.object.name === 'fbt'
34 | ) {
35 | path.node.callee.object.name = this.importName.name;
36 | }
37 | },
38 | },
39 | {importName},
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/fbt/generateTranslateFile.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const yargs = require('yargs');
3 | const path = require('path');
4 |
5 | const args = {
6 | SOURCE_STRINGS: 'source-strings',
7 | LOCALE: 'locale',
8 | OUT_DIRECTORY: 'out-directory',
9 | HELP: 'help',
10 | };
11 |
12 | const {argv} = yargs
13 | .usage('Generate or udpate a locale file with the proper skeleton')
14 | .string(args.SOURCE_STRINGS)
15 | .default(args.SOURCE_STRINGS, '.source_strings.json')
16 | .describe(
17 | args.SOURCE_STRINGS,
18 | 'The fbt source string file. default to ".source_strings"',
19 | )
20 | .alias(args.SOURCE_STRINGS, 's')
21 | .string(args.LOCALE)
22 | .demand(args.LOCALE, 'locale is required')
23 | .describe(
24 | args.LOCALE,
25 | 'locale of the translations to generate should be of the format xx-XX if you want to use navigator.language',
26 | )
27 | .alias(args.LOCALE, 'l')
28 | .string(args.OUT_DIRECTORY)
29 | .demand(args.OUT_DIRECTORY, 'output directory is required')
30 | .describe(
31 | args.LOCALE,
32 | 'directory where the translation file will be generated',
33 | )
34 | .alias(args.OUT_DIRECTORY, 'o')
35 | .alias(args.HELP, 'h');
36 |
37 | function extractHashToTexts(sourceStringsFile) {
38 | const {phrases} = JSON.parse(fs.readFileSync(sourceStringsFile));
39 |
40 | return phrases.map(phrase => phrase.hashToText);
41 | }
42 |
43 | function generateFile(locale, hashToTexts) {
44 | const content = {
45 | 'fb-locale': locale,
46 | translations: {},
47 | };
48 |
49 | hashToTexts.forEach((hashToText) => {
50 | Object.keys(hashToText).forEach((hash) => {
51 | const text = hashToText[hash];
52 |
53 | content.translations[hash] = {
54 | tokens: [],
55 | types: [],
56 | translations: [
57 | {
58 | translation: `#TODO ${text}`,
59 | variations: {},
60 | },
61 | ],
62 | };
63 | });
64 | });
65 |
66 | return content;
67 | }
68 |
69 | if (argv[args.HELP]) {
70 | yargs.showHelp();
71 | process.exit(0);
72 | }
73 |
74 | const hashToTexts = extractHashToTexts(argv[args.SOURCE_STRINGS]);
75 | const newOutput = generateFile(argv[args.LOCALE], hashToTexts);
76 | const outputFilePath = path.join(
77 | argv[args.OUT_DIRECTORY],
78 | `${argv[args.LOCALE]}.json`,
79 | );
80 |
81 | let output = newOutput;
82 |
83 | try {
84 | const existingFile = JSON.parse(fs.readFileSync(outputFilePath));
85 |
86 | output = {
87 | ...newOutput,
88 | translations: {
89 | ...newOutput.translations,
90 | ...existingFile.translations,
91 | },
92 | };
93 | }
94 | catch (e) {
95 | console.log('no existingFile');
96 | }
97 |
98 | fs.writeFileSync(
99 | path.join(argv[args.OUT_DIRECTORY], `${argv[args.LOCALE]}.json`),
100 | JSON.stringify(output, null, ' '),
101 | );
102 |
--------------------------------------------------------------------------------
/src/print.css:
--------------------------------------------------------------------------------
1 | @media print {
2 | @page {
3 | size: auto;
4 | margin: 0rem;
5 | }
6 |
7 | body {
8 | margin: 4rem 2rem;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/screens/App/ConditionalContent/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React from 'react';
3 |
4 | import ModalWithHoursAndDays from '../../../components/ModalWithHoursAndDays';
5 | import {useQuery} from '../../../utils/apollo-hooks';
6 | import {GET_USER_INFOS} from '../../../utils/queries';
7 |
8 | const ConditionalContentMain = styled('div')``;
9 |
10 | export default function ConditionalContent() {
11 | const {data, loading} = useQuery(GET_USER_INFOS, {suspend: false});
12 |
13 | if (loading) {
14 | return false;
15 | }
16 |
17 | const {workingDays, startWorkAt, endWorkAt} = data.me;
18 |
19 | return (
20 |
21 | {(!workingDays || !startWorkAt || !endWorkAt) && (
22 |
23 | )}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/screens/App/Dashboard/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled/macro';
2 | import React from 'react';
3 |
4 | import CreateTask from '../../../components/CreateTask';
5 | import HelpButton from '../../../components/HelpButton';
6 | import {BREAKPOINTS} from '../../../utils/constants';
7 | import {Container, Content, Main} from '../../../utils/new/design-system';
8 | import Tasks from './tasks';
9 |
10 | const Desktop = styled('div')`
11 | @media (max-width: ${BREAKPOINTS.mobile}px) {
12 | display: none;
13 | }
14 | `;
15 |
16 | function Dashboard() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default Dashboard;
33 |
--------------------------------------------------------------------------------
/src/screens/App/Onboarding/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React, {useState} from 'react';
3 | import {Redirect} from 'react-router-dom';
4 |
5 | import OnboardingCalendar from '../../../components/Onboarding/onboarding-calendar';
6 | import OnboardingCustomAssistant from '../../../components/Onboarding/onboarding-custom-assistant';
7 | import OnboardingFirstStep from '../../../components/Onboarding/onboarding-first-step';
8 | import OnboardingSkills from '../../../components/Onboarding/onboarding-skills';
9 | import OnboardingThirdStep from '../../../components/Onboarding/onboarding-third-step';
10 | import {useQuery} from '../../../utils/apollo-hooks';
11 | import {gray20, Loading, signalGreen} from '../../../utils/content';
12 | import {GET_USER_INFOS} from '../../../utils/queries';
13 |
14 | const OnboardingMain = styled('div')`
15 | max-width: 650px;
16 | margin-left: auto;
17 | margin-right: auto;
18 | margin-top: 100px;
19 | `;
20 |
21 | const OnboardingProgressBar = styled('div')`
22 | background: ${gray20};
23 | position: relative;
24 | height: 5px;
25 | width: 100%;
26 | margin-bottom: 15px;
27 |
28 | &:after {
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | content: ' ';
33 | width: ${props => props.completionRate || 0}%;
34 | height: 100%;
35 | background: ${signalGreen};
36 | transition: width 0.2s ease;
37 | }
38 | `;
39 |
40 | function Onboarding() {
41 | const {data, loading} = useQuery(GET_USER_INFOS, {suspend: true});
42 | const [currentStep, setStep] = useState(0);
43 |
44 | if (loading) return
;
45 |
46 | const {me} = data;
47 |
48 | function getNextStep() {
49 | setStep(currentStep + 1);
50 | }
51 |
52 | function getPreviousStep() {
53 | setStep(currentStep - 1);
54 | }
55 |
56 | const steps = [
57 | OnboardingFirstStep,
58 | OnboardingCustomAssistant,
59 | OnboardingCalendar,
60 | OnboardingSkills,
61 | OnboardingThirdStep,
62 | ];
63 |
64 | if (currentStep >= steps.length) {
65 | window.Intercom('trackEvent', 'start-onboarding-project');
66 | return
;
67 | }
68 |
69 | const CurrentOnboardingStep = steps[currentStep];
70 |
71 | return (
72 |
73 |
76 |
82 |
83 | );
84 | }
85 |
86 | export default Onboarding;
87 |
--------------------------------------------------------------------------------
/src/screens/App/Tags/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route, withRouter} from 'react-router-dom';
3 |
4 | import TagListForm from '../../../components/TagListForm';
5 | import fbt from '../../../fbt/fbt.macro';
6 | import {ModalContainer as Modal, ModalElem} from '../../../utils/content';
7 | import {SubHeading} from '../../../utils/new/design-system';
8 | import Dashboard from '../Dashboard';
9 | import TasksList from '../Tasks/tasks-lists';
10 |
11 | function Tags({location: {state = {}}, history}) {
12 | const Background
13 | = state.prevLocation && state.prevLocation.pathname.includes('tasks')
14 | ? TasksList
15 | : Dashboard;
16 | const prevLocation = state.prevLocation || {pathname: '/app/dashboard'};
17 |
18 | return (
19 | <>
20 |
} />
21 | history.push(prevLocation)}>
22 |
23 |
24 |
25 | Liste des tags
26 |
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
35 | export default withRouter(Tags);
36 |
--------------------------------------------------------------------------------
/src/screens/App/Tasks/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React, {Component} from 'react';
3 | import {Route} from 'react-router-dom';
4 |
5 | import TaskView from '../../../components/ItemView';
6 | import {ModalContainer as Modal, ModalElem} from '../../../utils/content';
7 | import TasksList from './tasks-lists';
8 |
9 | const ProjectMain = styled('div')`
10 | min-height: 100vh;
11 | flex: 1;
12 | margin-top: 3rem;
13 | `;
14 |
15 | class Tasks extends Component {
16 | render() {
17 | return (
18 |
19 |
20 | (
23 | history.push(
25 | `/app/tasks${state.prevSearch || ''}`,
26 | )
27 | }
28 | >
29 |
30 | history.push(
33 | `/app/tasks${state.prevSearch
34 | || ''}`,
35 | )
36 | }
37 | isActivating={state.isActivating}
38 | scheduledFor={state.scheduledFor}
39 | />
40 |
41 |
42 | )}
43 | />
44 |
45 | );
46 | }
47 | }
48 |
49 | export default Tasks;
50 |
--------------------------------------------------------------------------------
/src/screens/App/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React, {Suspense, useState} from 'react';
3 | import {Redirect, Route, Switch} from 'react-router-dom';
4 |
5 | import PendingActionsTray from '../../components/PendingActionsTray';
6 | import TrialHeadband from '../../components/TrialHeadband';
7 | import withHeader from '../../HOC/withHeader';
8 | import {BREAKPOINTS} from '../../utils/constants';
9 |
10 | const Account = React.lazy(() => import('./Account'));
11 | const CustomerList = React.lazy(() => import('./Customers'));
12 | const CollaboratorList = React.lazy(() => import('./Collaborators'));
13 | const CustomizeEmail = React.lazy(() => import('./CustomizeEmail'));
14 | const Dashboard = React.lazy(() => import('./Dashboard'));
15 | const Onboarding = React.lazy(() => import('./Onboarding'));
16 | const Projects = React.lazy(() => import('./Projects'));
17 | const Stats = React.lazy(() => import('./Stats'));
18 | const Tags = React.lazy(() => import('./Tags'));
19 | const Tasks = React.lazy(() => import('./Tasks'));
20 |
21 | const AppMain = styled('div')`
22 | display: flex;
23 | flex-direction: row;
24 |
25 | padding-top: ${props => (props.headband ? '45px' : '')};
26 |
27 | @media (max-width: ${BREAKPOINTS.mobile}px) {
28 | padding: 1rem;
29 | flex-direction: column;
30 | }
31 | `;
32 |
33 | function App({history, location}) {
34 | const [headband, setHeadband] = useState();
35 | return (
36 | <>
37 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
52 |
56 |
60 |
64 |
65 | }
68 | />
69 |
70 |
71 |
72 |
73 |
74 |
75 | >
76 | );
77 | }
78 |
79 | export default App;
80 |
--------------------------------------------------------------------------------
/src/screens/Customer/Tasks/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React from 'react';
3 | import {Route} from 'react-router-dom';
4 |
5 | import TaskView from '../../../components/ItemView';
6 | import {BREAKPOINTS} from '../../../utils/constants';
7 | import {ModalContainer as Modal, ModalElem} from '../../../utils/content';
8 | import CustomerTasks from './tasks';
9 |
10 | const Container = styled('div')`
11 | min-height: 100vh;
12 | padding: 3rem;
13 |
14 | @media (max-width: ${BREAKPOINTS.mobile}px) {
15 | padding: 1rem;
16 | }
17 | `;
18 |
19 | const Tasks = ({location, match}) => {
20 | const {customerToken} = match.params;
21 | const {prevSearch} = location.state || {prevSearch: location.search};
22 | const query = new URLSearchParams(prevSearch || location.search);
23 | const projectId = query.get('projectId');
24 |
25 | return (
26 |
27 |
28 |
29 | (
38 |
40 | history.push(
41 | `/app/${customerToken}/tasks${state.prevSearch ||
42 | prevSearch ||
43 | ''}`
44 | )
45 | }
46 | >
47 |
48 |
52 | history.push(
53 | `/app/${customerToken}/tasks${state.prevSearch ||
54 | ''}`
55 | )
56 | }
57 | />
58 |
59 |
60 | )}
61 | />
62 |
63 | );
64 | };
65 |
66 | export default Tasks;
67 |
--------------------------------------------------------------------------------
/src/screens/Customer/index.js:
--------------------------------------------------------------------------------
1 | import {IntlViewerContext} from 'fbt';
2 | import moment from 'moment';
3 | import React from 'react';
4 | import {Route, Switch} from 'react-router-dom';
5 |
6 | import {useQuery} from '../../utils/apollo-hooks';
7 | import {CustomerContext} from '../../utils/contexts';
8 | import {GET_CUSTOMER_LANGUAGE} from '../../utils/queries';
9 | import Quote from './Quote';
10 | import CustomerTasks from './Tasks';
11 |
12 | export default function Customer({match}) {
13 | const {data: dataLanguage} = useQuery(GET_CUSTOMER_LANGUAGE, {
14 | variables: {
15 | token: match.params.customerToken,
16 | },
17 | fetchPolicy: 'network-only',
18 | });
19 |
20 | if (
21 | dataLanguage
22 | && dataLanguage.customer
23 | && dataLanguage.customer.language
24 | ) {
25 | IntlViewerContext.locale
26 | = dataLanguage.customer.language === 'fr' ? 'fr-FR' : 'en-US';
27 | moment.locale(dataLanguage.customer.language);
28 | }
29 |
30 | return (
31 |
32 |
33 |
37 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/screens/Paid/index.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import React from 'react';
3 |
4 | import fbt from '../../fbt/fbt.macro';
5 | import {FlexColumn, FlexRow} from '../../utils/content';
6 | import Illus from '../../utils/images/bermuda-done.svg';
7 | import {ButtonLink, P} from '../../utils/new/design-system';
8 |
9 | const Container = styled('div')`
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | width: 100%;
14 | height: 800px;
15 | `;
16 |
17 | const IllusForPaying = styled('img')`
18 | height: 330px;
19 | `;
20 |
21 | const Column = styled(FlexColumn)`
22 | width: 400px;
23 | margin-left: 2rem;
24 | justify-content: center;
25 | `;
26 |
27 | function Paid({user}) {
28 | if (user && user.me) {
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | Merci pour votre confiance et bienvenue…!
36 |
37 | Retourner au dashboard
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 | Merci pour votre achat. Pour utiliser Inyo vous
54 | devez vous connecter.
55 |
56 | Se connecter
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default Paid;
65 |
--------------------------------------------------------------------------------
/src/screens/StraightToCheckout/index.js:
--------------------------------------------------------------------------------
1 | import {STRIPE_CONSTANT} from '../../utils/constants';
2 |
3 | const {stripeKey, ...stripeInfos} = STRIPE_CONSTANT;
4 |
5 | function StraightToCheckout({location}) {
6 | const stripe = window.Stripe(stripeKey);
7 | const queryString = new URLSearchParams(location.search);
8 |
9 | stripe
10 | .redirectToCheckout({
11 | ...stripeInfos,
12 | items: [stripeInfos.items[queryString.get('plan') || 'MONTHLY']],
13 | customerEmail: queryString.get('email'),
14 | clientReferenceId: queryString.get('id'),
15 | billingAddressCollection: 'required',
16 | })
17 | .then((result) => {
18 | if (result.error) {
19 | throw result.error;
20 | }
21 | });
22 | return false;
23 | }
24 |
25 | export default StraightToCheckout;
26 |
--------------------------------------------------------------------------------
/src/utils/accountSync/appleAccount.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/byte-foundry/inyo/967523566a0986fcd45e078f54f4cc46733e9ea9/src/utils/accountSync/appleAccount.js
--------------------------------------------------------------------------------
/src/utils/apollo-hooks.js:
--------------------------------------------------------------------------------
1 | import {useQuery as useApolloQuery} from '@apollo/react-hooks';
2 |
3 | import useBaseQuery from './useApolloQuery';
4 |
5 | export function useQuery(query, options) {
6 | /* eslint-disable */
7 | const result =
8 | options && options.suspend
9 | ? useBaseQuery(query, options)
10 | : useApolloQuery(query, options);
11 | /* eslint-enable */
12 |
13 | if (options && options.suspend && result.loading) {
14 | result.removeDataRefs();
15 | throw result.observable.result();
16 | }
17 |
18 | return result;
19 | }
20 |
21 | export {useApolloClient, useMutation} from '@apollo/react-hooks';
22 |
--------------------------------------------------------------------------------
/src/utils/calendars/appleCalendar.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/byte-foundry/inyo/967523566a0986fcd45e078f54f4cc46733e9ea9/src/utils/calendars/appleCalendar.js
--------------------------------------------------------------------------------
/src/utils/calendars/googleCalendar.js:
--------------------------------------------------------------------------------
1 | export default class GoogleCalendar {
2 | api = undefined;
3 |
4 | accountManager = undefined;
5 |
6 | constructor(accountManager) {
7 | this.accountManager = accountManager;
8 | }
9 |
10 | getEvents(calendarId, timeMin, timeMax) {
11 | if (this.accountManager.signedIn) {
12 | return this.accountManager.api.client.calendar.events.list({
13 | calendarId,
14 | timeMin,
15 | timeMax,
16 | singleEvents: true,
17 | });
18 | }
19 |
20 | return [];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/colors.js:
--------------------------------------------------------------------------------
1 | export const primaryWhite = '#ffffff';
2 | export const primaryBlue = '#5020ee';
3 | export const primaryNavyBlue = '#171a44';
4 | export const primarySalmon = '#fbada1';
5 | export const pastelGreen = '#e9fffd';
6 | export const secondaryRed = '#e62043';
7 | export const secondaryLightBlue = '#deebff';
8 | export const secondaryLightYellow = '#fffae6';
9 | export const secondaryLightGreen = '#e3fcef';
10 | export const gray10 = '#f9f9f9';
11 | export const gray20 = '#e0e0e0';
12 | export const gray30 = '#cccccc';
13 | export const gray50 = '#8c8c8c';
14 | export const gray70 = '#666666';
15 | export const gray80 = '#4d4d4d';
16 | export const alpha10 = 'rgba(0, 0, 0, 0.1)';
17 | export const collabGreen = '#cddc39';
18 | export const signalGreen = '#0dcc94';
19 | export const signalOrange = '#ffab00';
20 | export const signalRed = '#fe4a49';
21 | export const primaryPurple = '#5020ee';
22 | export const mediumPurple = '#8f76e0';
23 | export const lightPurple = '#f8f8ff';
24 | export const primaryRed = '#ff3366';
25 | export const lightRed = '#fff2f5';
26 | export const primaryGrey = '#A9A9A9';
27 | export const accentGrey = '#c7c7c7';
28 | export const lightGrey = '#F8F9FC';
29 | export const mediumGrey = '#f1f3f4';
30 | export const darkGrey = '#24272E';
31 | export const primaryBlack = '#140642';
32 |
--------------------------------------------------------------------------------
/src/utils/contexts.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const UserContext = React.createContext(undefined);
4 | export const CustomerContext = React.createContext(undefined);
5 |
--------------------------------------------------------------------------------
/src/utils/fonts/WorkSans-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/byte-foundry/inyo/967523566a0986fcd45e078f54f4cc46733e9ea9/src/utils/fonts/WorkSans-Italic.otf
--------------------------------------------------------------------------------
/src/utils/fragmentTypes.json:
--------------------------------------------------------------------------------
1 | {
2 | "__schema": {
3 | "types": [
4 | {
5 | "kind": "UNION",
6 | "name": "Author",
7 | "possibleTypes": [{"name": "User"}, {"name": "Customer"}]
8 | },
9 | {
10 | "kind": "UNION",
11 | "name": "Viewer",
12 | "possibleTypes": [{"name": "User"}, {"name": "Customer"}]
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/icons/appLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/utils/icons/drag.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/utils/icons/invoice-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/icons/inyo-topbar-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/icons/pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
8 |
9 |
--------------------------------------------------------------------------------
/src/utils/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/utils/icons/section-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/utils/icons/tags.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-collaborator-validated.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-collaborator.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-customer-validated.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-customer.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-personal.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon-user-validated.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/utils/icons/taskicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/utils/images/empty-clients-background.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/utils/images/empty-project-background.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/utils/images/empty-tasks-background.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/utils/images/google_g_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/acceptCollabRequest.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getUserCollaboratorsAndRequests: ({mutation, query}) => {
3 | const {requester} = mutation.result.data.acceptCollabRequest;
4 |
5 | return {
6 | ...query.result,
7 | me: {
8 | ...query.result.me,
9 | collaborators: [requester, ...query.result.me.collaborators],
10 | },
11 | };
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/createCustomer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getAllCustomers: ({mutation, query}) => {
3 | const addedCustomer = mutation.result.data.createCustomer;
4 |
5 | return {
6 | ...query.result,
7 | me: {
8 | ...query.result.me,
9 | customers: [addedCustomer, ...query.result.me.customers],
10 | },
11 | };
12 | },
13 | getUserCustomers: ({mutation, query}) => {
14 | const addedCustomer = mutation.result.data.createCustomer;
15 |
16 | return {
17 | ...query.result,
18 | me: {
19 | ...query.result.me,
20 | customers: [addedCustomer, ...query.result.me.customers],
21 | },
22 | };
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/createProject.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getAllProjects: ({mutation, query}) => {
3 | const addedProject = mutation.result.data.createProject;
4 |
5 | return {
6 | ...query.result,
7 | me: {
8 | ...query.result.me,
9 | projects: [addedProject, ...query.result.me.projects],
10 | },
11 | };
12 | },
13 | getAllTasksShort: ({mutation, query}) => {
14 | if (query.variables.schedule === 'UNSCHEDULED') {
15 | const {tasks} = query.result.me;
16 | const addedTasks = mutation.result.data.createProject.sections
17 | .map(section => section.items)
18 | .flat();
19 |
20 | return {
21 | ...query.result,
22 | me: {
23 | ...query.result.me,
24 | tasks: [...tasks, ...addedTasks],
25 | },
26 | };
27 | }
28 |
29 | return query.result;
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/createSection.js:
--------------------------------------------------------------------------------
1 | import reorderList from '../reorderList';
2 |
3 | export default {
4 | getAllTasksShort({mutation, query}) {
5 | const {tasks} = query.result.me;
6 | const addedSection = mutation.result.data.addSection;
7 |
8 | return {
9 | ...query.result,
10 | me: {
11 | ...query.result.me,
12 | tasks: [...tasks, ...addedSection.items],
13 | },
14 | };
15 | },
16 | getProjectData({mutation, query}) {
17 | const {project} = query.result;
18 | const addedSection = mutation.result.data.addSection;
19 |
20 | return {
21 | ...query.result,
22 | project: {
23 | ...project,
24 | sections: reorderList(
25 | project.sections,
26 | addedSection,
27 | null,
28 | addedSection.position,
29 | 'position',
30 | ),
31 | },
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/createTag.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getAllTags: ({mutation, query}) => {
3 | const {tags} = query.result.me;
4 | const addedTag = mutation.result.data.createTag;
5 |
6 | if (!tags.find(t => t.id === addedTag.id)) {
7 | return {
8 | ...query.result,
9 | me: {
10 | ...query.result.me,
11 | tags: [...tags, addedTag],
12 | },
13 | };
14 | }
15 | },
16 | userInfosQuery: ({mutation, query}) => {
17 | const {tags} = query.result.me;
18 | const addedTag = mutation.result.data.createTag;
19 |
20 | if (!tags.find(t => t.id === addedTag.id)) {
21 | return {
22 | ...query.result,
23 | me: {
24 | ...query.result.me,
25 | tags: [...tags, addedTag],
26 | },
27 | };
28 | }
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/deleteTask.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import reorderList from '../reorderList';
4 |
5 | const getProject = ({mutation, query}) => {
6 | const {project} = query.result;
7 | const removedItem = mutation.result.data.removeItem;
8 |
9 | const sections = project.sections.map((section) => {
10 | const itemIndex = section.items.findIndex(i => i.id === removedItem.id);
11 |
12 | if (itemIndex > -1) {
13 | return {
14 | ...section,
15 | items: reorderList(
16 | section.items,
17 | removedItem,
18 | itemIndex,
19 | null,
20 | 'position',
21 | ),
22 | };
23 | }
24 |
25 | return section;
26 | });
27 |
28 | return {
29 | ...query.result,
30 | project: {
31 | ...project,
32 | sections,
33 | },
34 | };
35 | };
36 |
37 | export default {
38 | getAllTasksShort: ({mutation, query}) => {
39 | const cachedItems = [...query.result.me.tasks];
40 | const removedItem = mutation.result.data.removeItem;
41 | const removedItemIndex = cachedItems.findIndex(
42 | item => item.id === removedItem.id,
43 | );
44 |
45 | cachedItems.splice(removedItemIndex, 1);
46 |
47 | return {
48 | ...query.result,
49 | me: {
50 | ...query.result.me,
51 | tasks: cachedItems,
52 | },
53 | };
54 | },
55 | getProjectData: getProject,
56 | getProjectInfos: getProject,
57 | getSchedule: ({mutation, query}) => {
58 | const taskId = mutation.variables.itemId;
59 |
60 | return produce(query.result, (draft) => {
61 | const {schedule} = draft.me;
62 |
63 | // remove old
64 | schedule.forEach((d) => {
65 | const filteredTasks = d.tasks.filter(t => t.id !== taskId);
66 |
67 | if (filteredTasks.length !== d.tasks.length) {
68 | filteredTasks.forEach((t, i) => {
69 | const scheduledFor = t.scheduledForDays.find(
70 | day => day.date === d.date,
71 | );
72 |
73 | if (scheduledFor) {
74 | scheduledFor.position = i;
75 | }
76 |
77 | t.schedulePosition = i;
78 | });
79 | }
80 |
81 | d.tasks = filteredTasks;
82 | });
83 | });
84 | },
85 | };
86 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/finishItem.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | import {isCustomerTask} from '../functions';
4 |
5 | export default {
6 | getPaymentAndCurrentTask: ({mutation, query}) => {
7 | const {currentTask} = query.result.me;
8 | const finishedTask = mutation.result.data.finishItem;
9 |
10 | if (currentTask && currentTask.id === finishedTask.id) {
11 | return {
12 | ...query.result,
13 | me: {
14 | ...query.result.me,
15 | currentTask: null,
16 | },
17 | };
18 | }
19 |
20 | return query.result;
21 | },
22 | getAllTasksShort: ({mutation, query}) => {
23 | const task = mutation.result.data.finishItem;
24 |
25 | return produce(query.result, (draft) => {
26 | if (!isCustomerTask(task.type)) {
27 | if (query.variables.schedule === 'TO_BE_RESCHEDULED') {
28 | draft.me.tasks = draft.me.tasks.filter(
29 | t => t.id !== task.id,
30 | );
31 | }
32 | else if (
33 | query.variables.schedule === 'FINISHED_TIME_IT_TOOK_NULL'
34 | ) {
35 | draft.me.tasks = draft.me.tasks.filter(
36 | t => t.id !== task.id,
37 | );
38 |
39 | if (
40 | task.status === 'FINISHED'
41 | && task.timeItTook === null
42 | ) {
43 | draft.me.tasks.push(task);
44 | }
45 | }
46 | }
47 | });
48 | },
49 | getSchedule: ({mutation, query}) => {
50 | const task = {...mutation.result.data.finishItem};
51 |
52 | return produce(query.result, (draft) => {
53 | const {schedule} = draft.me;
54 |
55 | if (!isCustomerTask(task.type)) return;
56 |
57 | schedule.forEach((scheduleDay) => {
58 | // remove old
59 | const filteredTasks = scheduleDay.tasks.filter(
60 | t => t.id !== task.id,
61 | );
62 |
63 | if (filteredTasks.length !== scheduleDay.tasks.length) {
64 | filteredTasks.forEach((t, i) => {
65 | t.schedulePosition = i;
66 | });
67 | }
68 |
69 | scheduleDay.tasks = filteredTasks;
70 |
71 | const scheduleLink = task.scheduledForDays.find(
72 | day => day.date === scheduleDay.date,
73 | );
74 |
75 | if (scheduleLink) {
76 | scheduleDay.tasks.splice(scheduleLink.position, 0, task);
77 | scheduleDay.tasks.forEach((t, i) => {
78 | t.schedulePosition = i;
79 | });
80 | }
81 | });
82 | });
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/issueQuote.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export default {
4 | getProjectQuotes: ({mutation, query}) => {
5 | const quoteCreated = mutation.result.data.issueQuote;
6 |
7 | return produce(query.result, (draft) => {
8 | draft.project.quotes.push(quoteCreated);
9 | });
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/removeAttachment.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export default {
4 | getProjectData: ({mutation, query}) => {
5 | const removedAttachment = mutation.result.data.removeFile;
6 | const queryResult = query.result;
7 |
8 | return produce(queryResult, (draft) => {
9 | const attachmentIndexInProject = draft.project.attachments.findIndex(
10 | attachment => attachment.id === removedAttachment.id,
11 | );
12 |
13 | if (attachmentIndexInProject !== -1) {
14 | draft.project.attachments.splice(attachmentIndexInProject, 1);
15 | }
16 |
17 | draft.project.sections.forEach((section) => {
18 | section.items.forEach((item) => {
19 | const attachmentIndex = item.attachments.findIndex(
20 | attachment => attachment.id === removedAttachment.id,
21 | );
22 |
23 | if (attachmentIndex !== -1) {
24 | item.attachments.splice(attachmentIndex, 1);
25 | }
26 | });
27 | });
28 | });
29 | },
30 | getItemDetails: ({mutation, query}) => {
31 | const {item} = query.result;
32 | const {attachments} = item.attachments;
33 | const removedAttachment = mutation.result.data.removeFile;
34 |
35 | if (!attachments.find(d => d.id === removedAttachment.id)) {
36 | return undefined;
37 | }
38 |
39 | return {
40 | ...query.result,
41 | item: {
42 | ...query.result.item,
43 | attachments: attachments.filter(
44 | f => f.id !== removedAttachment.id,
45 | ),
46 | },
47 | };
48 | },
49 | userInfosQuery: ({mutation, query}) => {
50 | const {me} = query.result;
51 | const removedFile = mutation.result.data.removeFile;
52 |
53 | if (!me.company.documents.find(d => d.id === removedFile.id)) {
54 | return undefined;
55 | }
56 |
57 | return {
58 | ...query.result,
59 | me: {
60 | ...me,
61 | company: {
62 | ...me.company,
63 | documents: me.company.documents.filter(
64 | d => d.id !== removedFile.id,
65 | ),
66 | },
67 | },
68 | };
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/removeCustomer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getAllCustomers: ({mutation, query}) => {
3 | const {me} = query.result;
4 | const customers = [...me.customers];
5 | const removedCustomer = mutation.result.data.removeCustomer;
6 | const indexToRemove = customers.findIndex(
7 | customer => customer.id === removedCustomer.id,
8 | );
9 |
10 | customers.splice(indexToRemove, 1);
11 |
12 | return {
13 | ...query.result,
14 | me: {
15 | ...me,
16 | customers,
17 | },
18 | };
19 | },
20 | getUserCustomers: ({mutation, query}) => {
21 | const {me} = query.result;
22 | const customers = [...me.customers];
23 | const removedCustomer = mutation.result.data.removeCustomer;
24 | const indexToRemove = customers.findIndex(
25 | customer => customer.id === removedCustomer.id,
26 | );
27 |
28 | customers.splice(indexToRemove, 1);
29 |
30 | return {
31 | ...query.result,
32 | me: {
33 | ...me,
34 | customers,
35 | },
36 | };
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/removeProject.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | const getAllTaskMutation = ({mutation, query}) => {
4 | const removedProject = mutation.result.data.removeProject;
5 | const tasks = removedProject.sections.reduce(
6 | (acc, section) => acc.concat(section.items.map(item => item.id)),
7 | [],
8 | );
9 |
10 | return produce(query.result, (draft) => {
11 | // remove old
12 | draft.me.tasks = draft.me.tasks.filter(t => !tasks.includes(t.id));
13 | });
14 | };
15 |
16 | export default {
17 | getAllTasksShort: args => getAllTaskMutation(args),
18 | getAllProjects({mutation, query}) {
19 | const {me} = query.result;
20 |
21 | const removedProject = mutation.result.data.removeProject;
22 | const projects = me.projects.filter(
23 | project => project.id !== removedProject.id,
24 | );
25 |
26 | return {
27 | ...query.result,
28 | me: {
29 | ...me,
30 | projects,
31 | },
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/removeSection.js:
--------------------------------------------------------------------------------
1 | import reorderList from '../reorderList';
2 |
3 | const getProject = ({mutation, query}) => {
4 | const {project} = query.result;
5 | const removedSection = mutation.result.data.removeSection;
6 |
7 | const sectionPosition = project.sections.findIndex(
8 | section => section.id === removedSection.id,
9 | );
10 |
11 | if (sectionPosition < 0) return null;
12 |
13 | return {
14 | ...query.result,
15 | project: {
16 | ...project,
17 | sections: reorderList(
18 | project.sections,
19 | removedSection,
20 | sectionPosition,
21 | null,
22 | 'position',
23 | ),
24 | },
25 | };
26 | };
27 |
28 | const getAllTasks = ({mutation, query}) => {
29 | const {me} = query.result;
30 |
31 | const removedSection = mutation.result.data.removeSection;
32 | const tasks = me.tasks.filter(
33 | task => !task.section || task.section.id !== removedSection.id,
34 | );
35 |
36 | return {
37 | ...query.result,
38 | me: {
39 | ...me,
40 | tasks,
41 | },
42 | };
43 | };
44 |
45 | export default {
46 | getProjectData: getProject,
47 | getProjectInfos: getProject,
48 | getAllTasksShort: getAllTasks,
49 | getAllTasksStats: getAllTasks,
50 | };
51 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/removeTag.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getAllTags: ({mutation, query}) => {
3 | const cachedTags = query.result.me.tags;
4 | const removedTag = mutation.result.data.removeTag;
5 | const tags = cachedTags.filter(tag => tag.id !== removedTag.id);
6 |
7 | return {
8 | ...query.result,
9 | me: {
10 | ...query.result.me,
11 | tags,
12 | },
13 | };
14 | },
15 | userInfosQuery: ({mutation, query}) => {
16 | const cachedTags = query.result.me.tags;
17 | const removedTag = mutation.result.data.removeTag;
18 | const tags = cachedTags.filter(tag => tag.id !== removedTag.id);
19 |
20 | return {
21 | ...query.result,
22 | me: {
23 | ...query.result.me,
24 | tags,
25 | },
26 | };
27 | },
28 | getAllTasksShort: ({mutation, query}) => {
29 | const cachedItems = query.result.me.tasks;
30 | const removedTag = mutation.result.data.removeTag;
31 |
32 | const newItems = cachedItems.map(item => ({
33 | ...item,
34 | tags: item.tags.filter(tag => tag.id !== removedTag.id),
35 | }));
36 |
37 | return {
38 | ...query.result,
39 | me: {
40 | ...query.result.me,
41 | tasks: newItems,
42 | },
43 | };
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/requestCollab.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getUserCollaboratorsAndRequests: ({mutation, query}) => {
3 | const {collaboratorRequests} = query.result.me;
4 | const addedRequestCollab = mutation.result.data.requestCollab;
5 |
6 | return {
7 | ...query.result,
8 | me: {
9 | ...query.result.me,
10 | collaboratorRequests: [
11 | addedRequestCollab,
12 | ...collaboratorRequests,
13 | ],
14 | },
15 | };
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/startTaskTimer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getPaymentAndCurrentTask: ({mutation, query}) => ({
3 | ...query.result,
4 | me: {
5 | ...query.result.me,
6 | currentTask: mutation.result.data.startTaskTimer,
7 | },
8 | }),
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/stopCurrentTaskTimer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getPaymentAndCurrentTask: ({query}) => ({
3 | ...query.result,
4 | me: {
5 | ...query.result.me,
6 | currentTask: null,
7 | },
8 | }),
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/unfinishItem.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 | import moment from 'moment';
3 |
4 | import {isCustomerTask} from '../functions';
5 |
6 | export default {
7 | getAllTasksShort: ({mutation, query}) => {
8 | const task = mutation.result.data.unfinishItem;
9 |
10 | return produce(query.result, (draft) => {
11 | if (!isCustomerTask(task.type)) {
12 | if (
13 | (query.variables.schedule === 'TO_BE_RESCHEDULED'
14 | && (task.status !== 'FINISHED'
15 | && task.scheduledFor
16 | && moment(task.scheduledFor).isBefore(
17 | moment(),
18 | 'day',
19 | )))
20 | || task.scheduledForDays.some(
21 | d => moment(d.date).isBefore(moment(), 'day')
22 | && d.status !== 'FINISHED',
23 | )
24 | ) {
25 | draft.me.tasks = draft.me.tasks.filter(
26 | t => t.id !== task.id,
27 | );
28 |
29 | draft.me.tasks.push(task);
30 | }
31 | else if (
32 | query.variables.schedule === 'FINISHED_TIME_IT_TOOK_NULL'
33 | ) {
34 | draft.me.tasks = draft.me.tasks.filter(
35 | t => t.id !== task.id,
36 | );
37 | }
38 | }
39 | });
40 | },
41 | getSchedule: ({mutation, query}) => {
42 | const task = {...mutation.result.data.unfinishItem};
43 |
44 | return produce({task, result: query.result}, (draft) => {
45 | const {
46 | task,
47 | result: {
48 | me: {schedule},
49 | },
50 | } = draft;
51 |
52 | // remove old
53 | schedule.forEach((d) => {
54 | const filteredTasks = d.tasks.filter(t => t.id !== task.id);
55 |
56 | if (filteredTasks.length !== d.tasks.length) {
57 | filteredTasks.forEach((t, i) => {
58 | const scheduledFor = t.scheduledForDays.find(
59 | day => day.date === d.date,
60 | );
61 |
62 | if (scheduledFor) {
63 | scheduledFor.position = i;
64 | }
65 |
66 | t.schedulePosition = i;
67 | });
68 | }
69 |
70 | d.tasks = filteredTasks;
71 | });
72 | });
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/unfocusTask.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export default {
4 | getAllTasksShort: ({mutation, query}) => {
5 | const task = mutation.result.data.unfocusTask;
6 | const {tasks} = query.result.me;
7 |
8 | const outdatedTaskIndex = tasks.findIndex(item => item.id === task.id);
9 |
10 | // adding to unscheduled list
11 | if (
12 | task.scheduledForDays.length === 0
13 | && query.variables.schedule === 'UNSCHEDULED'
14 | ) {
15 | return {
16 | ...query.result,
17 | me: {
18 | ...query.result.me,
19 | tasks:
20 | outdatedTaskIndex >= 0
21 | ? tasks.splice(outdatedTaskIndex, 1, task)
22 | : [task, ...tasks],
23 | },
24 | };
25 | }
26 |
27 | // removing from to be rescheduled list
28 | if (
29 | task.scheduledForDays.length === 0
30 | && query.variables.schedule === 'TO_BE_RESCHEDULED'
31 | ) {
32 | return {
33 | ...query.result,
34 | me: {
35 | ...query.result.me,
36 | tasks: tasks.filter(t => t.id !== task.id),
37 | },
38 | };
39 | }
40 |
41 | if (
42 | query.variables.schedule === 'FINISHED_TIME_IT_TOOK_NULL'
43 | && task.status === 'FINISHED'
44 | && task.timeItTook === null
45 | ) {
46 | if (tasks.findIndex(t => t.id === task.id) !== -1) {
47 | return query.result;
48 | }
49 |
50 | return {
51 | ...query.result,
52 | me: {
53 | ...query.result.me,
54 | tasks: [...tasks, task],
55 | },
56 | };
57 | }
58 | },
59 | getSchedule: ({mutation, query}) => {
60 | const task = mutation.result.data.unfocusTask;
61 | const {from} = mutation.variables;
62 |
63 | return produce(query.result, (draft) => {
64 | const {schedule} = draft.me;
65 |
66 | // remove old
67 | schedule.forEach((d) => {
68 | if (from && from !== d.date) return;
69 |
70 | const filteredTasks = d.tasks.filter(t => t.id !== task.id);
71 |
72 | if (filteredTasks.length !== d.tasks.length) {
73 | filteredTasks.forEach((t, i) => {
74 | const scheduledFor = t.scheduledForDays.find(
75 | day => day.date === d.date,
76 | );
77 |
78 | if (scheduledFor) {
79 | scheduledFor.position = i;
80 | }
81 |
82 | t.schedulePosition = i;
83 | });
84 | }
85 |
86 | d.tasks = filteredTasks;
87 | });
88 | });
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/updateProject.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export default {
4 | getProjectQuotes: ({mutation, query}) => {
5 | const {quoteHeader, quoteFooter} = mutation.result.data.updateProject;
6 |
7 | return produce(query.result, (draft) => {
8 | draft.project.quoteHeader = quoteHeader;
9 | draft.project.quoteFooter = quoteFooter;
10 | });
11 | },
12 | getAllTasksShort: ({mutation, query}) => {
13 | const cachedTasks = query.result.me.tasks;
14 |
15 | const addedCustomer = mutation.result.data.updateProject.customer;
16 |
17 | const tasks = cachedTasks.map((task) => {
18 | if (
19 | task.section
20 | && task.section.project.id === mutation.variables.projectId
21 | ) {
22 | return {
23 | ...task,
24 | linkedCustomer: addedCustomer && {
25 | id: addedCustomer.id,
26 | name: addedCustomer.name,
27 | },
28 | section: {
29 | ...task.section,
30 | project: {
31 | ...task.section.project,
32 | customer: addedCustomer && {
33 | id: addedCustomer.id,
34 | name: addedCustomer.name,
35 | },
36 | },
37 | },
38 | };
39 | }
40 |
41 | return task;
42 | });
43 |
44 | return {
45 | ...query.result,
46 | me: {
47 | ...query.result.me,
48 | tasks,
49 | },
50 | };
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/updateSection.js:
--------------------------------------------------------------------------------
1 | import reorderList from '../reorderList';
2 |
3 | const getProject = ({mutation, query}) => {
4 | const {project} = query.result;
5 | const updatedSection = mutation.result.data.updateSection;
6 |
7 | const oldSectionIndex = project.sections.findIndex(
8 | s => s.id === updatedSection.id,
9 | );
10 |
11 | if (oldSectionIndex < 0) return null;
12 |
13 | return {
14 | ...query.result,
15 | project: {
16 | ...project,
17 | sections: reorderList(
18 | project.sections,
19 | updatedSection,
20 | oldSectionIndex,
21 | updatedSection.position,
22 | 'position',
23 | ),
24 | },
25 | };
26 | };
27 |
28 | export default {
29 | getProjectData: getProject,
30 | getProjectInfos: getProject,
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/updateUser.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getUsersInfos({mutation, query}) {
3 | const updatedUser = mutation.result.data.updateUser;
4 |
5 | return {
6 | ...query.result,
7 | me: {
8 | ...query.result.me,
9 | me: updatedUser,
10 | },
11 | };
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/mutationLinks/uploadAttachments.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | const updateProjectData = ({mutation, query}) => {
4 | const uploadedAttachments = mutation.result.data.uploadAttachments;
5 | const {taskId, projectId} = mutation.variables;
6 | const queryResult = query.result;
7 |
8 | if (taskId) {
9 | return produce(queryResult, (draft) => {
10 | draft.project.sections.forEach((section) => {
11 | section.items.forEach((item) => {
12 | uploadedAttachments.forEach((attachment) => {
13 | if (
14 | item.id === taskId
15 | && !item.attachments.find(a => attachment.id === a.id)
16 | ) {
17 | item.attachments.push(attachment);
18 | }
19 | });
20 | });
21 | });
22 | });
23 | }
24 | if (projectId) {
25 | return produce(queryResult, (draft) => {
26 | uploadedAttachments.forEach((attachment) => {
27 | if (
28 | projectId === draft.project.id
29 | && !draft.project.attachments.find(a => attachment.id === a.id)
30 | ) {
31 | draft.project.attachments.push(attachment);
32 | }
33 | });
34 | });
35 | }
36 | };
37 |
38 | export default {
39 | getItemDetails: ({mutation, query}) => {
40 | const uploadedAttachments = mutation.result.data.uploadAttachments;
41 | const {taskId} = mutation.variables;
42 |
43 | if (taskId) {
44 | return produce(query.result, (draft) => {
45 | uploadedAttachments.forEach((attachment) => {
46 | if (
47 | !draft.item.attachments.find(
48 | a => attachment.id === a.id,
49 | )
50 | ) {
51 | draft.item.attachments.push(attachment);
52 | }
53 | });
54 | });
55 | }
56 | },
57 | getProjectData: updateProjectData,
58 | getProjectDataWithToken: updateProjectData,
59 | };
60 |
--------------------------------------------------------------------------------
/src/utils/reorderList.js:
--------------------------------------------------------------------------------
1 | const reorderList = (
2 | list,
3 | item,
4 | currentIndex,
5 | newIndex,
6 | positionPropertyName = null,
7 | ) => {
8 | const newList = [...list];
9 | let start;
10 | let end;
11 |
12 | if (currentIndex < 0 || currentIndex > list.length) {
13 | return list;
14 | }
15 |
16 | // creation
17 | if (currentIndex === null) {
18 | // check to avoid duplicates
19 | if (
20 | typeof newIndex === 'number'
21 | && list[newIndex]
22 | && list[newIndex].id === item.id
23 | ) {
24 | return list;
25 | }
26 |
27 | newList.splice(newIndex, 0, item);
28 |
29 | start = newIndex + 1;
30 | end = newList.length;
31 | }
32 | // remove
33 | else if (newIndex === null) {
34 | // check to avoid duplicates
35 | if (
36 | typeof currentIndex === 'number'
37 | && list[currentIndex]
38 | && list[currentIndex].id !== item.id
39 | ) {
40 | return list;
41 | }
42 |
43 | newList.splice(currentIndex, 1);
44 |
45 | start = currentIndex;
46 | end = newList.length;
47 | }
48 | // update
49 | else {
50 | newList.splice(currentIndex, 1);
51 | newList.splice(newIndex, 0, item);
52 |
53 | start = currentIndex < newIndex ? currentIndex : newIndex;
54 | end = currentIndex < newIndex ? newIndex + 1 : currentIndex + 1;
55 | }
56 |
57 | if (positionPropertyName) {
58 | for (let i = start; i < end; i++) {
59 | newList[i] = {
60 | ...newList[i],
61 | [positionPropertyName]: i,
62 | };
63 | }
64 | }
65 |
66 | return newList;
67 | };
68 |
69 | export default reorderList;
70 |
--------------------------------------------------------------------------------
/src/utils/useAccount.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useMemo, useState} from 'react';
2 |
3 | import GoogleAccount from './accountSync/googleAccount';
4 |
5 | const useAccount = () => {
6 | const [signedIn, setSignedIn] = useState(false);
7 | const [userInfo, setUserInfo] = useState({});
8 | const [loading, setLoading] = useState(false);
9 | const googleAccount = useMemo(
10 | () => GoogleAccount.instance(setSignedIn, setUserInfo, setLoading),
11 | [setSignedIn, setUserInfo, setLoading],
12 | );
13 |
14 | useEffect(
15 | () => () => googleAccount.unsubscribe(setSignedIn, setUserInfo, setLoading),
16 | [],
17 | );
18 |
19 | return [googleAccount, signedIn, userInfo, loading];
20 | };
21 |
22 | export default useAccount;
23 |
--------------------------------------------------------------------------------
/src/utils/useApolloQuery.js:
--------------------------------------------------------------------------------
1 | import {getApolloContext} from '@apollo/react-common';
2 | import {QueryData} from '@apollo/react-hooks/lib/data/QueryData';
3 | import {useDeepMemo} from '@apollo/react-hooks/lib/utils/useDeepMemo';
4 | import {useContext, useEffect, useState} from 'react';
5 |
6 | // TODO: might be better in a context
7 | const queryDataRefs = new Map();
8 | const forceUpdateRefs = new Set();
9 |
10 | // taken from https://github.com/apollographql/react-apollo/blob/master/packages/hooks/src/utils/useBaseQuery.ts
11 | export default function useBaseQuery(query, options) {
12 | const context = useContext(getApolloContext());
13 | const [tick, forceUpdate] = useState(0);
14 | const updatedOptions = options ? {...options, query} : {query};
15 |
16 | // we used the serialized options to keep track of the queries
17 | const queryKey = JSON.stringify(updatedOptions);
18 |
19 | if (!queryDataRefs.has(queryKey)) {
20 | queryDataRefs.set(
21 | queryKey,
22 | new QueryData({
23 | options: updatedOptions,
24 | context,
25 | forceUpdate: () => {
26 | const snapshotOfForceUpdate = Array.from(
27 | forceUpdateRefs.values(),
28 | );
29 |
30 | snapshotOfForceUpdate.forEach((fn) => {
31 | fn(x => x + 1);
32 | });
33 | },
34 | }),
35 | );
36 | }
37 |
38 | const queryData = queryDataRefs.get(queryKey);
39 |
40 | queryData.setOptions(updatedOptions);
41 | queryData.context = context;
42 |
43 | forceUpdateRefs.add(forceUpdate);
44 |
45 | // `onError` and `onCompleted` callback functions will not always have a
46 | // stable identity, so we'll exclude them from the memoization key to
47 | // prevent `afterExecute` from being triggered un-necessarily.
48 | const memo = {
49 | options: Object.assign(Object.assign({}, updatedOptions), {
50 | onError: undefined,
51 | onCompleted: undefined,
52 | }),
53 | context,
54 | tick,
55 | };
56 |
57 | const result = useDeepMemo(() => queryData.execute(), memo);
58 |
59 | const queryResult = result;
60 |
61 | useEffect(() => queryData.afterExecute({lazy: false}), [
62 | queryResult.loading,
63 | queryResult.networkStatus,
64 | queryResult.error,
65 | queryResult.data,
66 | ]);
67 |
68 | useEffect(
69 | () => () => {
70 | queryData.cleanup();
71 |
72 | queryDataRefs.delete(queryKey);
73 | forceUpdateRefs.delete(forceUpdate);
74 | },
75 | [],
76 | );
77 |
78 | const removeDataRefs = () => {
79 | forceUpdateRefs.delete(forceUpdate);
80 | };
81 |
82 | result.observable = queryData.currentObservable.query;
83 | result.removeDataRefs = removeDataRefs;
84 |
85 | return result;
86 | }
87 |
--------------------------------------------------------------------------------
/src/utils/useCalendar.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import {useEffect, useState} from 'react';
3 |
4 | import GoogleCalendar from './calendars/googleCalendar';
5 | import {getEventFromGoogleCalendarEvents} from './functions';
6 | import useUserInfos from './useUserInfos';
7 |
8 | let cache = {};
9 |
10 | const useCalendar = (account, calendarOptions) => {
11 | const calendarManager = new GoogleCalendar(account);
12 | const [cacheVersion, setCacheVersion] = useState(0);
13 | const {startWorkAt, endWorkAt, workingTime} = useUserInfos();
14 | const cacheKey = [
15 | ...calendarOptions,
16 | workingTime,
17 | startWorkAt,
18 | endWorkAt
19 | ].join('_');
20 |
21 | if (
22 | account &&
23 | account.api &&
24 | account.api.auth2 &&
25 | account.api.auth2.getAuthInstance()
26 | ) {
27 | account.api.auth2.getAuthInstance().isSignedIn.listen(isSignedIn => {
28 | if (!isSignedIn) {
29 | cache = {};
30 | setCacheVersion(x => x + 1);
31 | }
32 | });
33 | }
34 |
35 | useEffect(() => {
36 | if (!cache[cacheKey].loaded && account.signedIn) {
37 | const effect = async () => {
38 | const googleEvents = getEventFromGoogleCalendarEvents(
39 | await calendarManager.getEvents(...calendarOptions),
40 | startWorkAt,
41 | endWorkAt,
42 | workingTime
43 | );
44 |
45 | cache[cacheKey].data = googleEvents;
46 | cache[cacheKey].loaded = true;
47 | setCacheVersion(x => x + 1);
48 | };
49 |
50 | effect();
51 | }
52 | }, [
53 | account.signedIn,
54 | ...calendarOptions,
55 | workingTime,
56 | startWorkAt,
57 | endWorkAt
58 | ]);
59 |
60 | if (!cache[cacheKey]) {
61 | cache[cacheKey] = {data: {}, loaded: false};
62 | }
63 |
64 | return cache[cacheKey];
65 | };
66 |
67 | export default useCalendar;
68 |
--------------------------------------------------------------------------------
/src/utils/useEmailData.js:
--------------------------------------------------------------------------------
1 | import {useQuery} from './apollo-hooks';
2 | import {GET_EMAIL_TYPES} from './queries';
3 |
4 | const useEmailData = () => {
5 | const {data, loading, error} = useQuery(GET_EMAIL_TYPES);
6 |
7 | if (!data || !data.emailTypes || loading || error) {
8 | return {error};
9 | }
10 |
11 | const categories = [];
12 |
13 | data.emailTypes.forEach((type) => {
14 | let catIndex = categories.findIndex(c => c.name === type.category);
15 |
16 | if (catIndex === -1) {
17 | categories.push({
18 | name: type.category,
19 | types: [],
20 | });
21 | catIndex = categories.length - 1;
22 | }
23 |
24 | categories[catIndex].types.push(type);
25 | });
26 | return {
27 | categories,
28 | };
29 | };
30 |
31 | export default useEmailData;
32 |
--------------------------------------------------------------------------------
/src/utils/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 |
3 | export default function useLocalStorage(key, initialValue) {
4 | // State to store our value
5 | // Pass initial state function to useState so logic is only executed once
6 | const [storedValue, setStoredValue] = useState(() => {
7 | try {
8 | // Get from local storage by key
9 | const item = window.localStorage.getItem(key);
10 | // Parse stored json or if none return initialValue
11 |
12 | return item ? JSON.parse(item) : initialValue;
13 | }
14 | catch (error) {
15 | // If error also return initialValue
16 | console.log(error);
17 | return initialValue;
18 | }
19 | });
20 |
21 | // Return a wrapped version of useState's setter function that ...
22 | // ... persists the new value to localStorage.
23 | const setValue = (value) => {
24 | try {
25 | // Allow value to be a function so we have same API as useState
26 | const valueToStore
27 | = value instanceof Function ? value(storedValue) : value;
28 | // Save state
29 |
30 | setStoredValue(valueToStore);
31 | // Save to local storage
32 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
33 | }
34 | catch (error) {
35 | // A more advanced implementation would handle the error case
36 | console.log(error);
37 | }
38 | };
39 |
40 | return [storedValue, setValue];
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils/useMeasure.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef, useState} from 'react';
2 | import ResizeObserver from 'resize-observer-polyfill';
3 |
4 | export default function useMeasure() {
5 | const ref = useRef();
6 | const [bounds, set] = useState({
7 | left: 0,
8 | top: 0,
9 | width: 0,
10 | height: 0,
11 | });
12 | const [ro] = useState(
13 | () => new ResizeObserver(([entry]) => set(entry.contentRect)),
14 | );
15 |
16 | useEffect(() => {
17 | if (ref.current) ro.observe(ref.current);
18 | return () => ro.disconnect();
19 | }, [ro]);
20 | return [{ref}, bounds];
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/useOnClickOutside.js:
--------------------------------------------------------------------------------
1 | import arePassiveEventsSupported from 'are-passive-events-supported';
2 | import {useEffect} from 'react';
3 |
4 | export default function useOnClickOutside(ref, handler) {
5 | useEffect(() => {
6 | const listener = (event) => {
7 | // Do nothing if clicking ref's element or descendent elements, or if element is not in the DOM tree
8 | if (
9 | !ref.current
10 | || ref.current.contains(event.target)
11 | || !document.body.contains(event.target)
12 | ) {
13 | return;
14 | }
15 |
16 | handler(event);
17 | };
18 |
19 | document.addEventListener('mousedown', listener);
20 | document.addEventListener(
21 | 'touchstart',
22 | listener,
23 | arePassiveEventsSupported() ? {passive: true} : undefined,
24 | );
25 |
26 | return () => {
27 | document.removeEventListener('mousedown', listener);
28 | document.removeEventListener(
29 | 'touchstart',
30 | listener,
31 | arePassiveEventsSupported() ? {passive: true} : undefined,
32 | );
33 | };
34 | }, [ref, handler]);
35 | }
36 |
--------------------------------------------------------------------------------
/src/utils/usePrevious.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef} from 'react';
2 |
3 | export default function usePrevious(value) {
4 | const ref = useRef();
5 |
6 | useEffect(() => {
7 | ref.current = value;
8 | }, [value]);
9 | return ref.current;
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/useScheduleData.jsx:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import {useQuery} from './apollo-hooks';
3 | import {GET_ALL_TASKS_SHORT, GET_SCHEDULE} from './queries';
4 |
5 | const useScheduleData = ({startingFrom} = {}) => {
6 | const {data: dataTasks, loading: loadingTasks} = useQuery(
7 | GET_ALL_TASKS_SHORT,
8 | {
9 | variables: {schedule: 'TO_BE_RESCHEDULED'},
10 | pollInterval: 1000 * 60 * 10 // refresh tasks every 10 min
11 | }
12 | );
13 | const {data, loading} = useQuery(GET_SCHEDULE, {
14 | fetchPolicy: 'cache-and-network',
15 | variables: {start: startingFrom},
16 | pollInterval: 1000 * 60 * 10 // refresh tasks every 10 min
17 | });
18 |
19 | return useMemo(() => {
20 | if (
21 | (loading && !(data && data.me && data.me.schedule)) ||
22 | loadingTasks
23 | ) {
24 | return {
25 | loading: true,
26 | scheduledTasksPerDay: {},
27 | tasksToReschedule: []
28 | };
29 | }
30 |
31 | const {
32 | me: {id, schedule}
33 | } = data;
34 |
35 | const scheduledTasksPerDay = {};
36 |
37 | schedule.forEach(day => {
38 | scheduledTasksPerDay[day.date] = {
39 | ...day,
40 | deadlines: day.deadlines.map(d => ({
41 | ...d,
42 | project: d.projectStatus ? d : undefined,
43 | task: d.status ? d : undefined
44 | })),
45 | tasks: day.tasks.filter(
46 | t => !(t.owner.id === id && t.assignee)
47 | ),
48 | assignedTasks: day.tasks.filter(
49 | t => t.owner.id === id && t.assignee
50 | )
51 | };
52 | });
53 |
54 | return {
55 | scheduledTasksPerDay,
56 | tasksToReschedule: dataTasks.me.tasks
57 | };
58 | }, [data, loading, dataTasks, loadingTasks]);
59 | };
60 |
61 | export default useScheduleData;
62 |
--------------------------------------------------------------------------------
/src/utils/useUserInfos.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | import {useQuery} from './apollo-hooks';
4 | import {GET_USER_INFOS} from './queries';
5 |
6 | export default function useUserInfos() {
7 | const {data, loading, error} = useQuery(GET_USER_INFOS);
8 |
9 | if (!data || !data.me || loading || error) {
10 | return {};
11 | }
12 |
13 | const {
14 | id,
15 | startWorkAt,
16 | endWorkAt,
17 | startBreakAt,
18 | endBreakAt,
19 | workingDays,
20 | settings,
21 | defaultDailyPrice,
22 | clientViews,
23 | lastName,
24 | firstName,
25 | company: {
26 | vat, vatRate, siret, rcs,
27 | },
28 | } = data.me;
29 |
30 | let workingTime = null;
31 |
32 | if (startWorkAt && endWorkAt) {
33 | const diffTime = moment(endWorkAt, 'HH:mm:ss').diff(
34 | moment(startWorkAt, 'HH:mm:ss'),
35 | 'hours',
36 | true,
37 | );
38 |
39 | workingTime = diffTime < 0 ? diffTime + 24 : diffTime;
40 | }
41 |
42 | if (startBreakAt && endBreakAt) {
43 | const diffTime = moment(endBreakAt, 'HH:mm:ss').diff(
44 | moment(startBreakAt, 'HH:mm:ss'),
45 | 'hours',
46 | true,
47 | );
48 |
49 | const breakTime = diffTime < 0 ? diffTime + 24 : diffTime;
50 |
51 | workingTime -= breakTime;
52 | }
53 |
54 | return {
55 | id,
56 | startWorkAt,
57 | endWorkAt,
58 | workingTime,
59 | workingDays,
60 | defaultDailyPrice,
61 | clientViews,
62 | language: settings.language,
63 | hasFullWeekSchedule: settings.hasFullWeekSchedule,
64 | assistantName: settings.assistantName,
65 | lastName,
66 | firstName,
67 | vat,
68 | vatRate,
69 | siret,
70 | rcs,
71 | };
72 | }
73 |
--------------------------------------------------------------------------------