diff --git a/ComApp/android/app/src/main/res/drawable/launch_screen.png b/ComApp/android/app/src/main/res/drawable/launch_screen.png
new file mode 100644
index 0000000..9449be2
Binary files /dev/null and b/ComApp/android/app/src/main/res/drawable/launch_screen.png differ
diff --git a/ComApp/android/app/src/main/res/layout/launch_screen.xml b/ComApp/android/app/src/main/res/layout/launch_screen.xml
new file mode 100644
index 0000000..323780c
--- /dev/null
+++ b/ComApp/android/app/src/main/res/layout/launch_screen.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/ComApp/android/app/src/main/res/values/colors.xml b/ComApp/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..97d7b59
--- /dev/null
+++ b/ComApp/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #000000
+ #FFFFFF
+
diff --git a/ComApp/android/app/src/main/res/values/ic_launcher_background.xml b/ComApp/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..cfa9be0
--- /dev/null
+++ b/ComApp/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/ComApp/package-lock.json b/ComApp/package-lock.json
index a6d22cf..b8fa537 100644
--- a/ComApp/package-lock.json
+++ b/ComApp/package-lock.json
@@ -30,6 +30,7 @@
"react-native-b-crypt-bridge": "file:react-native-b-crypt-bridge-2.0.0.tgz",
"react-native-blob-util": "^0.16.2",
"react-native-flash-message": "^0.3.1",
+ "react-native-gesture-handler": "^2.7.0",
"react-native-pdf": "^6.6.2",
"react-native-progress": "^5.0.0",
"react-native-safe-area-context": "^4.4.1",
@@ -2014,6 +2015,17 @@
"node": ">=0.1.95"
}
},
+ "node_modules/@egjs/hammerjs": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
+ "dependencies": {
+ "@types/hammerjs": "^2.0.36"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
@@ -4390,6 +4402,11 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hammerjs": {
+ "version": "2.0.41",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
+ "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -16870,6 +16887,22 @@
"react-native": "*"
}
},
+ "node_modules/react-native-gesture-handler": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.7.0.tgz",
+ "integrity": "sha512-0jr3FNm2R3gv/v6XTtENgjv0fewD6LEct8EWmXw/oHw36M3YiIIpxnW57thL+0YiKwyLBXN0QHL4JZbs/heW2Q==",
+ "dependencies": {
+ "@egjs/hammerjs": "^2.0.17",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-gradle-plugin": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz",
@@ -21586,6 +21619,14 @@
"minimist": "^1.2.0"
}
},
+ "@egjs/hammerjs": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
+ "requires": {
+ "@types/hammerjs": "^2.0.36"
+ }
+ },
"@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
@@ -23457,6 +23498,11 @@
"@types/node": "*"
}
},
+ "@types/hammerjs": {
+ "version": "2.0.41",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
+ "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
+ },
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -32440,6 +32486,18 @@
"react-native-iphone-x-helper": "^1.3.1"
}
},
+ "react-native-gesture-handler": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.7.0.tgz",
+ "integrity": "sha512-0jr3FNm2R3gv/v6XTtENgjv0fewD6LEct8EWmXw/oHw36M3YiIIpxnW57thL+0YiKwyLBXN0QHL4JZbs/heW2Q==",
+ "requires": {
+ "@egjs/hammerjs": "^2.0.17",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.7.2"
+ }
+ },
"react-native-gradle-plugin": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz",
diff --git a/ComApp/package.json b/ComApp/package.json
index bc5c2a6..0298301 100644
--- a/ComApp/package.json
+++ b/ComApp/package.json
@@ -32,6 +32,7 @@
"react-native-b-crypt-bridge": "file:react-native-b-crypt-bridge-2.0.0.tgz",
"react-native-blob-util": "^0.16.2",
"react-native-flash-message": "^0.3.1",
+ "react-native-gesture-handler": "^2.7.0",
"react-native-pdf": "^6.6.2",
"react-native-progress": "^5.0.0",
"react-native-safe-area-context": "^4.4.1",
diff --git a/ComApp/src/App.js b/ComApp/src/App.js
index 7a93db9..970ca30 100644
--- a/ComApp/src/App.js
+++ b/ComApp/src/App.js
@@ -1,51 +1,167 @@
-/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * @format
- * @flow strict-local
- */
-
+import 'react-native-gesture-handler';
import React from 'react';
-import { View, Image, TextInput, TouchableOpacity } from 'react-native';
-// Redux
+import { createStore } from 'redux';
import { Provider } from 'react-redux';
-// Store
-import { store } from './App/Store/ConfigureStore';
-// Navigation
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import CustomNavigation from './Navigation';
-// Icons
-import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
-// Flash Message
-import FlashMessage from 'react-native-flash-message';
-// Translations
-import i18n from "./I18n";
-// Navigations style
-import styles from './Resources/Styles/NavigationStyle';
-// Database
-import { Setting } from "./Database";
-// Views
-import InstallScreen from './App/Views/InstallScreen';
-import HomeScreen from './App/Views/HomeScreen';
-import LoginScreen from './App/Views/LoginScreen';
-import SyncScreen from './App/Views/SyncScreen';
-import DirectoriesScreen from './App/Views/DirectoriesScreen';
-import PDFScreen from './App/Views/PDFScreen';
-import VideoScreen from './App/Views/VideoScreen';
-import OnlyVideosScreen from './App/Views/OnlyVideosScreen';
-import ContactScreen from './App/Views/ContactScreen';
-import ContactDetailsScreen from './App/Views/ContactDetailsScreen';
-import SearchScreen from './App/Views/SearchScreen';
-import MySpaceScreen from './App/Views/MySpaceScreen';
-// Lang Flags
-import flagFR from './Resources/assets/img/flags/fr.png';
-import flagEN from './Resources/assets/img/flags/en.png';
-import flagDE from './Resources/assets/img/flags/de.png';
-import flagES from './Resources/assets/img/flags/es.png';
-import SyncController from './App/Controllers/SyncController';
+import { TouchableOpacity, View, Image, Text, TextInput, StyleSheet, Alert } from 'react-native';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import FlashMessage, { showMessage } from 'react-native-flash-message';
+
+import LoginScreen from './Views/Login';
+import HomeScreen from './Views/Home';
+import DirectoriesScreen from './Views/Directories';
+import VideoScreen from './Views/Video';
+import PDFScreen from './Views/PDF';
+import SyncScreen from './Views/Sync';
+import ContactScreen from './Views/Contact';
+import ContactDetailsScreen from './Views/ContactDetails';
+import OnlyVideosScreen from './Views/OnlyVideos';
+import SearchScreen from "./Views/Search";
+import MySpaceScreen from './Views/MySpace';
+
+import UsersModel from './Models/Users';
+import DirectoriesModel from './Models/Directories';
+import DocumentsModel from './Models/Documents';
+import ContactsModel from './Models/Contacts';
+import SendBrochureModel from './Models/SendBrochure';
+import CommonModel from './Models/Common';
+
+import Sync from './Controllers/Sync';
+import Helpers from "./Controllers/Helpers";
+import i18n from "./I18n";
+
+const initialState = {
+ locale: 'fr',
+ users: {},
+ directories: {},
+ documents: {},
+ contacts: {},
+ syncVersion: 0,
+ logged: undefined,
+ currentUser: {},
+ currentOpenedDoc: {},
+ ShowIsActive: 0,
+};
+
+function reducer(state = initialState, action) {
+ //console.log('reducer', state, action);
+ //console.log('DISPATCH');
+
+ switch (action.type) {
+ case 'INIT':
+ let docs = DocumentsModel.getAll(DirectoriesModel.getAll(
+ state.currentUser.role,
+ state.currentUser.authorizedFolders,
+ false,
+ state.locale
+ ));
+ return {
+ ...state,
+ users: UsersModel.getAll(),
+ directories: DirectoriesModel.getAll(
+ state.currentUser.role,
+ state.currentUser.authorizedFolders,
+ true,
+ state.locale
+ ),
+ documents: docs.filter(item => item.lang === state.locale),
+ contacts: ContactsModel.getByUser(state.currentUser.id),
+ ShowIsActive: CommonModel.get('ShowIsActive')['value'],
+ };
+ case 'INIT_USERS':
+ return {
+ ...state,
+ users: UsersModel.getAll(),
+ };
+ case 'SET_CURRENT_USER':
+ return {
+ ...state,
+ currentUser: JSON.parse(action.data),
+ };
+ case 'SET_LOGGED':
+ if (action.data === false) {
+ CommonModel.delete('isLogged');
+ }
+ return {
+ ...state,
+ logged: action.data,
+ };
+ case 'UPDATE_LOCALE':
+ let nxt;
+ if (action.data === undefined) {
+ const availableLocales = ['fr', 'en', 'de'];
+ if (availableLocales.indexOf(i18n.locale) === availableLocales.length - 1) {
+ nxt = availableLocales[0];
+ } else {
+ nxt = availableLocales[availableLocales.indexOf(i18n.locale) + 1];
+ }
+ } else {
+ nxt = action.data;
+ }
+ i18n.locale = nxt;
+ CommonModel.update({ key: 'locale', value: nxt });
+ let docsL = DocumentsModel.getAll(DirectoriesModel.getAll(
+ state.currentUser.role,
+ state.currentUser.authorizedFolders,
+ false,
+ nxt
+ ));
+ return {
+ ...state,
+ locale: nxt,
+ directories: DirectoriesModel.getAll(
+ state.currentUser.role,
+ state.currentUser.authorizedFolders,
+ true,
+ nxt
+ ),
+ documents: docsL.filter(item => item.lang === nxt),
+ };
+ case 'SET_CURRENT_OPENED_DOC':
+ return {
+ ...state,
+ currentOpenedDoc: action.data
+ };
+ case 'UPDATE_VERSION':
+ return {
+ ...state,
+ syncVersion: action.data,
+ };
+ case 'UPDATE_SHOW_IS_ACTIVE':
+ Sync.updateShowIsActive(action.data);
+ return {
+ ...state,
+ ShowIsActive: action.data,
+ };
+ case 'UPDATE_CONTACT':
+ ContactsModel.update(action.data);
+ return {
+ ...state,
+ contacts: ContactsModel.getByUser(state.currentUser.id),
+ };
+ case 'ADD_CONTACT':
+ ContactsModel.add(action.data);
+ return {
+ ...state,
+ contacts: ContactsModel.getByUser(state.currentUser.id),
+ };
+ case 'DELETE_CONTACT':
+ ContactsModel.delete(action.data);
+ return {
+ ...state,
+ contacts: ContactsModel.getByUser(state.currentUser.id),
+ };
+ case 'SEND_BROCHURE':
+ SendBrochureModel.add(action.data);
+ return state;
+ default:
+ return state;
+ }
+}
+
+const store = createStore(reducer);
const Stack = createNativeStackNavigator();
store.dispatch({ type: 'INIT_USERS' });
@@ -58,56 +174,134 @@ class App extends React.Component {
ShowIsActive: '0'
};
- componentDidMount() {
- store.subscribe(this.handleChange.bind(this));
- if (SyncController.isInstalled() && SyncController.isLogged()) {
+ componentDidMount(): void {
+ const unsubscribe = store.subscribe(this.handleChange.bind(this));
+ if (Sync.isInstalled() && Sync.isLogged()) {
store.dispatch({
type: 'SET_CURRENT_USER',
- data: Setting.get('isLogged').value,
+ data: CommonModel.get('isLogged').value,
});
store.dispatch({ type: 'SET_LOGGED', data: true });
}
- const savedLocale = Setting.get('locale');
+ const savedLocale = CommonModel.get('locale');
if (savedLocale !== undefined && savedLocale !== false) {
store.dispatch({ type: 'UPDATE_LOCALE', data: savedLocale.value });
}
}
+ handleChange() {
+ const logged = store.getState().logged;
+ if (logged === false) {
+ CommonModel.delete('isLogged');
+ }
+ if (logged !== undefined) {
+ this.setState({ logged: logged });
+ }
+ this.setState({ ShowIsActive: store.getState().ShowIsActive });
+ }
+
+ LogoTitle(title) {
+ return (
+
+
+ {title}
+
+ );
+ }
+
+ SendEmail() {
+ return (
+
+ this.setState({ sendEmail: text })} />
+ {
+ if (this.state.sendEmail.length > 0) {
+ if (Helpers.isEmailValid(this.state.sendEmail)) {
+ const email = {
+ email: this.state.sendEmail,
+ subject: i18n.t('app.file_sharing'),
+ message: '',
+ documents: JSON.stringify([store.getState().currentOpenedDoc]),
+ user_id: store.getState().currentUser.id
+ };
+ store.dispatch({ type: 'SEND_BROCHURE', data: email });
+ showMessage({
+ message: i18n.t('global.message_sent') + '.',
+ description: i18n.t('global.message_will_be_sent_after_sync') + '.',
+ type: 'success',
+ duration: 1500,
+ });
+ this.setState({ sendEmail: '' });
+ } else {
+ Alert.alert(i18n.t('global.warning') + ' !', i18n.t('error.invalid_email_address') + '.', [
+ { text: 'OK' },
+ ],
+ { cancelable: false },
+ );
+ }
+ }
+ }}>
+
+
+
+ );
+ }
+
render() {
- const savedLocale = Setting.get('locale');
+ const savedLocale = CommonModel.get('locale');
let locale;
if (savedLocale !== undefined) {
locale = savedLocale.value;
} else {
- locale = store.getState().app.locale;
+ locale = store.getState().locale;
}
-
let flag;
- switch (locale) {
- case "fr":
- flag = { uri: Image.resolveAssetSource(flagFR).uri }
+ switch (locale) {
+ case "fr":
+ flag = require('./assets/img/flags/fr.png');
break;
- case "en":
- flag = { uri: Image.resolveAssetSource(flagEN).uri }
+ case "en":
+ flag = require('./assets/img/flags/en.png');
break;
- case "de":
- flag = { uri: Image.resolveAssetSource(flagDE).uri }
+ case "de":
+ flag = require('./assets/img/flags/de.png');
break;
- case "es":
- flag = { uri: Image.resolveAssetSource(flagES).uri }
+ case "es":
+ flag = require('./assets/img/flags/es.png');
break;
- }
-
- return (
-
-
-
- {this.state.logged === false ? this.renderInstallOrLogin(flag) : (
+ }
+ return (
+
+
+
+ {this.state.logged === false ? (
+ ({
+ headerTitle: () => this.LogoTitle(i18n.t('global.login')),
+ headerRight: () => (
+
+ {
+ store.dispatch({ type: 'UPDATE_LOCALE' });
+ this.setState({ state: this.state }); // to force re-rendering
+ }}
+ style={{ marginRight: 15 }}>
+
+
+
+ ),
+ })}
+ />
+ ) : (
({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('global.home')),
+ headerTitle: () => this.LogoTitle(i18n.t('global.home')),
headerRight: () => (
@@ -121,7 +315,7 @@ class App extends React.Component {
this.setState({ searchTxt: '' });
}
}}>
-
+
navigation.navigation.navigate('MySpaceScreen')}
style={{ marginRight: 15 }}>
-
+
)}
-
+
{
store.dispatch({ type: 'SET_LOGGED', data: false });
}}
style={{ marginRight: 15 }}>
-
+
- )
+ ),
})}
/>
- )}
- CustomNavigation.LogoTitle(i18n.t('global.synchronization')) }}
- />
- CustomNavigation.LogoTitle(i18n.t('app.folders')) }}
- />
- ({
- headerTitle: () => CustomNavigation.LogoTitle('PDF')
- })}
- />
- ({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('app.video'))
- })}
- />
- ({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('global.videos')),
- headerRight: () => (
-
- this.setState({ searchTxt: text })} />
- {
- if (this.state.searchTxt.length > 0) {
- navigation.navigation.navigate('SearchScreen', {
- needle: this.state.searchTxt,
- onlyVideos: true,
- });
- this.setState({ searchTxt: '' });
- }
- }}>
-
-
-
- ),
- })}
- />
- CustomNavigation.LogoTitle(i18n.t('global.contacts')) }}
- initialParams={{ contactAdded: false }}
- />
- CustomNavigation.LogoTitle(i18n.t('app.contact_details')) }}
- />
- CustomNavigation.LogoTitle(i18n.t('app.results_search')) }}
- />
- ({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('app.my_space')),
- headerRight: () => (
-
- {
- const folder = store.getState().directories.filter(tmp => tmp.id === 131);
- navigation.navigation.navigate('DirectoriesScreen', { folder: folder })
- }}
- style={{ marginRight: 15 }}>
-
-
-
- ),
- })}
- />
-
-
-
-
- );
- }
-
- handleChange() {
- const logged = store.getState().app.logged;
- if (logged === false) {
- Setting.delete('isLogged');
- }
- if (logged !== undefined) {
- this.setState({ logged: logged });
- }
- this.setState({ ShowIsActive: store.getState().app.ShowIsActive });
- }
-
- renderInstallOrLogin(flag) {
- if (SyncController.isInstalled()) {
- return (
- ({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('global.login')),
- headerRight: () => (
-
- {
- store.dispatch({ type: 'UPDATE_LOCALE' });
- this.setState({ state: this.state }); // to force re-rendering
- }}
- style={{ marginRight: 15 }}>
-
-
-
- ),
- })}
- />
- );
- } else {
- return (
- ({
- headerTitle: () => CustomNavigation.LogoTitle(i18n.t('install.installation')),
- headerRight: () => (
-
- {
- store.dispatch({ type: 'UPDATE_LOCALE' });
- this.setState({ state: this.state }); // to force re-rendering
- }}
- style={{ marginRight: 15 }}>
-
-
-
- ),
- })}
- />
- );
- }
+ )}
+ this.LogoTitle(i18n.t('global.synchronization')) }}
+ />
+ this.LogoTitle(i18n.t('app.folders')) }}
+ />
+ ({
+ headerTitle: () => this.LogoTitle(i18n.t('global.videos')),
+ headerRight: () => (
+
+ this.setState({ searchTxt: text })} />
+ {
+ if (this.state.searchTxt.length > 0) {
+ navigation.navigation.navigate('SearchScreen', {
+ needle: this.state.searchTxt,
+ onlyVideos: true,
+ });
+ this.setState({ searchTxt: '' });
+ }
+ }}>
+
+
+
+ ),
+ })}
+ />
+ ({
+ headerTitle: () => this.LogoTitle(i18n.t('app.video')),
+ headerRight: () => this.SendEmail()
+ })}
+ />
+ ({
+ headerTitle: () => this.LogoTitle('PDF'),
+ headerRight: () => this.SendEmail()
+ })}
+ />
+ this.LogoTitle(i18n.t('global.contacts')) }}
+ initialParams={{ contactAdded: false }}
+ />
+ this.LogoTitle(i18n.t('app.contact_details')) }}
+ />
+ this.LogoTitle(i18n.t('app.results_search')) }}
+ />
+ ({
+ headerTitle: () => this.LogoTitle(i18n.t('app.my_space')),
+ headerRight: () => (
+
+ {
+ const folder = store.getState().directories.filter(tmp => tmp.id === 131);
+ navigation.navigation.navigate('DirectoriesScreen', { folder: folder })
+ }}
+ style={{ marginRight: 15 }}>
+
+
+
+ ),
+ })}
+ />
+
+
+
+
+ );
}
}
-export default App;
\ No newline at end of file
+const styles = StyleSheet.create({
+ searchInput: {
+ borderWidth: 1,
+ borderColor: '#ce3029',
+ width: 200,
+ paddingLeft: 20,
+ },
+ btnSearch: {
+ width: 50,
+ height: 50,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#ce3029'
+ }
+});
+
+export default App;
diff --git a/ComApp/src/Controllers/Helpers.js b/ComApp/src/Controllers/Helpers.js
new file mode 100644
index 0000000..7e55261
--- /dev/null
+++ b/ComApp/src/Controllers/Helpers.js
@@ -0,0 +1,47 @@
+class Helpers {
+ compareValues(key, order = 'asc') {
+ return function innerSort(a, b) {
+ if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
+ // property doesn't exist on either object
+ return 0;
+ }
+
+ const varA = (typeof a[key] === 'string')
+ ? a[key].toUpperCase() : a[key];
+ const varB = (typeof b[key] === 'string')
+ ? b[key].toUpperCase() : b[key];
+
+ let comparison = 0;
+ if (varA > varB) {
+ comparison = 1;
+ } else if (varA < varB) {
+ comparison = -1;
+ }
+ return (
+ (order === 'desc') ? (comparison * -1) : comparison
+ );
+ };
+ }
+
+ isEmailValid(email) {
+ const expression = /(?!.*\.{2})^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([\t]*\r\n)?[\t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([\t]*\r\n)?[\t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i;
+ return expression.test(String(email).toLowerCase());
+ }
+
+ generateUUID() { // Public Domain/MIT
+ let d = new Date().getTime();//Timestamp
+ let d2 = new Date().getUTCMilliseconds() + Math.random();//Time in microseconds since page-load or 0 if unsupported
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = Math.random() * 16;//random number between 0 and 16
+ if(d > 0){//Use timestamp until depleted
+ r = (d + r)%16 | 0;
+ d = Math.floor(d/16);
+ } else {//Use microseconds since page-load if supported
+ r = (d2 + r)%16 | 0;
+ d2 = Math.floor(d2/16);
+ }
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
+ });
+ }
+}
+export default new Helpers();
diff --git a/ComApp/src/Controllers/Sync.js b/ComApp/src/Controllers/Sync.js
new file mode 100644
index 0000000..1893368
--- /dev/null
+++ b/ComApp/src/Controllers/Sync.js
@@ -0,0 +1,270 @@
+import NetInfo from '@react-native-community/netinfo';
+import Constants from 'expo-constants';
+
+const axios = require('axios');
+const api_url = 'https://app.maitrecoq.fr';
+const api_token = 'Np5smPey2q94HxY8xV27c2TN7C6VpC';
+const endpoint = api_url + '/api';
+const app_uuid = '1f6eda31-1f6d-49fe-aa81-4f0aece0f7ca';
+
+import UsersModel from '../Models/Users';
+import DirectoriesModel from '../Models/Directories';
+import DocumentsModel from '../Models/Documents';
+import ContactsModel from '../Models/Contacts';
+import CommonModel from '../Models/Common';
+import SendBrochureModel from '../Models/SendBrochure';
+
+class Sync {
+ getEndpointDocs() {
+ return api_url + '/storage/docs/';
+ }
+
+ getApiToken() {
+ return api_token;
+ }
+
+ getAppUUID() {
+ return app_uuid;
+ }
+
+ getVersion(callback, errorCallback) {
+ axios({
+ method: 'get',
+ url: endpoint + '/version',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ app_uuid: app_uuid,
+ app_version: Constants.nativeAppVersion
+ },
+ })
+ .then(response => {
+ return callback(response.data.version, response.data.ShowIsActive, response.data.newApp);
+ })
+ .catch(function(error) {
+ console.log(error);
+ return errorCallback(error);
+ });
+ }
+
+ needSync(callback) {
+ NetInfo.fetch().then(state => {
+ if (state.isConnected) {
+ axios({
+ method: 'get',
+ url: endpoint + '/version',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ },
+ })
+ .then(response => {
+ const localVersion = CommonModel.get('version');
+ if (
+ localVersion === undefined ||
+ localVersion === false ||
+ localVersion.value !== response.data.version
+ ) {
+ return callback();
+ }
+ return false;
+ })
+ .catch(function(error) {
+ console.log(error);
+ });
+ }
+ return false;
+ });
+ return false;
+ }
+
+ setInstalled() {
+ CommonModel.add({key: 'installed', value: '1'});
+ CommonModel.add({key: 'locale', value: 'fr'});
+ CommonModel.add({key: 'ShowIsActive', value: '0'});
+ }
+
+ getUsers(callback) {
+ axios({
+ method: 'get',
+ url: endpoint + '/users',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ },
+ })
+ .then(response => {
+ UsersModel.clear();
+ response.data.forEach(user => {
+ UsersModel.add(user);
+ });
+ console.log('Users Update Done');
+ return callback();
+ })
+ .catch(function(error) {
+ console.log(error);
+ });
+ }
+
+ getDirectories(callback) {
+ axios({
+ method: 'get',
+ url: endpoint + '/directories',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ },
+ })
+ .then(function(response) {
+ DirectoriesModel.clear();
+ response.data.forEach(dir => {
+ DirectoriesModel.add(dir);
+ });
+ console.log('Directories Update Done');
+ return callback();
+ })
+ .catch(function(error) {
+ console.log(error);
+ });
+ }
+
+ getDocuments(callback) {
+ axios({
+ method: 'get',
+ url: endpoint + '/documents',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ },
+ })
+ .then(response => {
+ const toDelete = DocumentsModel.clear(response.data);
+ response.data.forEach(document => {
+ DocumentsModel.update(document);
+ });
+ console.log('Documents Update Done');
+ return callback(DocumentsModel.getAll(), toDelete);
+ })
+ .catch(function(error) {
+ console.log(error);
+ });
+ }
+
+ getContacts(callback, userId) {
+ let contacts = ContactsModel.getAll();
+ axios({
+ method: 'post',
+ url: endpoint + '/contacts' + '?token=' + this.getApiToken(),
+ responseType: 'json',
+ data: {
+ contacts: contacts,
+ userId: userId,
+ },
+ })
+ .then(response => {
+ ContactsModel.clear();
+ response.data.forEach(contact => {
+ ContactsModel.add(contact);
+ });
+ console.log('Contacts Update Done');
+ callback();
+ })
+ .catch(function(error) {
+ console.log(error);
+ });
+ }
+
+ async sendBrochure(brochures, i) {
+ if (brochures.length > 0) {
+ const item = brochures[i];
+ let response = await axios({
+ method: 'post',
+ url: endpoint + '/sendBrochure',
+ responseType: 'json',
+ params: {
+ token: this.getApiToken(),
+ emails: {item},
+ },
+ });
+ if (i < brochures.length - 1) {
+ return this.sendBrochure(brochures, i + 1);
+ }
+ return true;
+ }
+ return true;
+ }
+
+ sendBrochures(callback) {
+ this.sendBrochure(SendBrochureModel.getAll(), 0).then(res => {
+ SendBrochureModel.clear();
+ console.log('SendBrochures Update Done');
+ callback();
+ });
+ }
+
+ updateVersion(version) {
+ const localVersion = CommonModel.get('version');
+ if (localVersion === false || localVersion === undefined) {
+ CommonModel.add({key: 'version', value: version});
+ } else {
+ CommonModel.update({id: localVersion.id, key: 'version', value: version});
+ }
+ }
+
+ updateShowIsActive(value) {
+ const local = CommonModel.get('ShowIsActive');
+ if (local === false || local === undefined) {
+ CommonModel.add({key: 'ShowIsActive', value: value});
+ } else {
+ CommonModel.update({id: local.id, key: 'ShowIsActive', value: value});
+ }
+ }
+
+ isLogged() {
+ const logged = CommonModel.get('isLogged');
+ const loggedTime = CommonModel.get('loggedTime');
+ if (
+ logged !== false &&
+ loggedTime !== false &&
+ logged !== undefined &&
+ loggedTime !== undefined
+ ) {
+ let seconds =
+ Math.abs(new Date(Number(loggedTime.value)) - Date.now()) / 1000;
+ if (seconds < 14400) {
+ // 4h
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ isInstalled() {
+ const test = CommonModel.get('installed');
+ if (test === false) {
+ return false;
+ }
+ return true;
+ }
+
+ updateLastModificationDoc(id, date) {
+ DocumentsModel.update({id: id, updated_at: date.toString()}, true);
+ }
+
+ isSynced() {
+ const SendBrochure = SendBrochureModel.getAll();
+ const Contacts = ContactsModel.getAll();
+
+ if (SendBrochure !== undefined && Contacts !== undefined) {
+ const noSynced = Contacts.filtered('synced = false');
+ if (SendBrochure.length > 0 || noSynced.length > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
+
+export default new Sync();
diff --git a/ComApp/src/Database.js b/ComApp/src/Database.js
index 0c6f026..e2fb0f5 100644
--- a/ComApp/src/Database.js
+++ b/ComApp/src/Database.js
@@ -1,127 +1,103 @@
-import Realm from "realm";
-// Models
-import SettingModel from "./App/Models/Setting";
-import UserModel from './App/Models/User';
-import ContactModel from './App/Models/Contact';
-import SendBrochureModel from "./App/Models/SendBrochure";
-import DirectoryModel from "./App/Models/Directory";
-import DocumentModel from './App/Models/Document';
-
-const SettingSchema = {
- name: 'Settings',
- primaryKey: 'id',
- properties: {
- id: 'int',
- key: 'string',
- value: 'string',
- },
-};
-
-const UserSchema = {
- name: 'Users',
- primaryKey: 'id',
- properties: {
- id: 'int',
- email: 'string',
- surname: 'string',
- firstname: 'string',
- password: 'string',
- role: 'string',
- authorizedFolders: 'string',
- },
-};
-
-const ContactSchema = {
- name: 'Contacts',
- primaryKey: 'id',
- properties: {
- id: 'int',
- email: 'string',
- type: 'string',
- other: { type: 'string', optional: true },
- select1: { type: 'string', optional: true },
- select2: { type: 'string', optional: true },
- name: 'string',
- firstname: 'string',
- company: { type: 'string', optional: true },
- address: { type: 'string', optional: true },
- phone: 'string',
- products: { type: 'string', optional: true },
- // TODO : card_url ?
- notes: { type: 'string', optional: true },
- visits: { type: 'string', optional: true },
- user_id: 'int',
- uniq_id: 'string',
- to_delete: { type: 'bool', default: false },
- synced: { type: 'bool', default: true },
- },
-};
-
-const SendBrochureSchema = {
- name: 'SendBrochures',
- primaryKey: 'id',
- properties: {
- id: 'int',
- email: 'string',
- subject: 'string',
- message: 'string',
- documents: 'string',
- user_id: 'int'
- },
-};
-
-const DirectorySchema = {
- name: 'Directories',
- primaryKey: 'id',
- properties: {
- id: 'int',
- name: 'string',
- parent: { type: 'int', optional: true },
- order: 'int',
- needAuthorization: 'int',
- },
-};
-
-const DocumentSchema = {
- name: 'Documents',
- primaryKey: 'id',
- properties: {
- id: 'int',
- uuid: 'string',
- title: 'string',
- type: 'string',
- path: 'string',
- directory: 'int',
- lang: 'string',
- updated_at: 'string',
- },
-};
-
-const Database = {
- schema: [
- SettingSchema,
- UserSchema,
- ContactSchema,
- SendBrochureSchema,
- DirectorySchema,
- DocumentSchema
- ],
- //encryptionKey: new Int8Array(64)
- };
-
-let realm = new Realm(Database);
-let Setting = new SettingModel(realm);
-let User = new UserModel(realm);
-let Contact = new ContactModel(realm);
-let SendBrochure = new SendBrochureModel(realm);
-let Directory = new DirectoryModel(realm);
-let Document = new DocumentModel(realm);
-
-export {
- Setting,
- User,
- Contact,
- SendBrochure,
- Directory,
- Document
-}
\ No newline at end of file
+const CommonSchema = {
+ name: 'Common',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ key: 'string',
+ value: 'string',
+ },
+};
+
+const UserSchema = {
+ name: 'Users',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ email: 'string',
+ surname: 'string',
+ firstname: 'string',
+ password: 'string',
+ role: 'string',
+ authorizedFolders: 'string',
+ },
+};
+
+const DirectoriesSchema = {
+ name: 'Directories',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ name: 'string',
+ parent: { type: 'int', optional: true },
+ order: 'int',
+ needAuthorization: 'int',
+ },
+};
+
+const DocumentsSchema = {
+ name: 'Documents',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ uuid: 'string',
+ title: 'string',
+ type: 'string',
+ path: 'string',
+ directory: 'int',
+ lang: 'string',
+ updated_at: 'string',
+ },
+};
+
+const ContactsSchema = {
+ name: 'Contacts',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ email: 'string',
+ type: 'string',
+ other: { type: 'string', optional: true },
+ select1: { type: 'string', optional: true },
+ select2: { type: 'string', optional: true },
+ name: 'string',
+ firstname: 'string',
+ company: { type: 'string', optional: true },
+ address: { type: 'string', optional: true },
+ phone: 'string',
+ products: { type: 'string', optional: true },
+ // TODO : card_url ?
+ notes: { type: 'string', optional: true },
+ visits: { type: 'string', optional: true },
+ user_id: 'int',
+ uniq_id: 'string',
+ to_delete: { type: 'bool', default: false },
+ synced: { type: 'bool', default: true },
+ },
+};
+
+const SendBrochureSchema = {
+ name: 'SendBrochure',
+ primaryKey: 'id',
+ properties: {
+ id: 'int',
+ email: 'string',
+ subject: 'string',
+ message: 'string',
+ documents: 'string',
+ user_id: 'int'
+ },
+};
+
+const Database = {
+ schema: [
+ CommonSchema,
+ UserSchema,
+ DirectoriesSchema,
+ DocumentsSchema,
+ ContactsSchema,
+ SendBrochureSchema,
+ ],
+ //encryptionKey: new Int8Array(64)
+};
+
+export default Database;
diff --git a/ComApp/src/I18n.js b/ComApp/src/I18n.js
index 45fed4e..3b94187 100644
--- a/ComApp/src/I18n.js
+++ b/ComApp/src/I18n.js
@@ -1,17 +1,18 @@
-import { I18n } from 'i18n-js';
-
-import fr from './Resources/Locales/fr';
-import en from './Resources/Locales/en';
-import de from './Resources/Locales/de';
-
-const i18n = new I18n({
- fr: fr,
- en: en,
- de: de
-});
-
-i18n.defaultLocale = 'fr';
-i18n.locale = 'fr';
-i18n.enableFallback = true;
-
-export default i18n;
+import { I18n } from 'i18n-js';
+
+import fr from './Locales/fr';
+import en from './Locales/en';
+import de from './Locales/de';
+
+const i18n = new I18n({
+ fr: fr,
+ en: en,
+ de: de
+});
+
+i18n.defaultLocale = 'fr';
+i18n.locale = 'fr';
+i18n.fallbacks = true;
+i18n.translations = { fr, en, de };
+
+export default i18n;
diff --git a/ComApp/src/Locales/de.json b/ComApp/src/Locales/de.json
new file mode 100644
index 0000000..bc56844
--- /dev/null
+++ b/ComApp/src/Locales/de.json
@@ -0,0 +1,157 @@
+{
+ "global": {
+ "yes": "Ja",
+ "no": "Nein",
+ "home": "Startseite",
+ "search": "Suche",
+ "login": "Verbindung",
+ "email": "Email",
+ "message_sent": "Email geschickt",
+ "message_will_be_sent_after_sync": "Die Nachricht wird nach Synchronisation geschickt",
+ "warning": "Achtung",
+ "synchronization": "Synchronisation ",
+ "videos": "Videos",
+ "contacts": "Kontakt",
+ "confirmation": "Bestätigung",
+ "name": "Name",
+ "firstname": "Vorname",
+ "address": "Adresse",
+ "phone": "Telefonnummer",
+ "company": "Firma",
+ "add": "Addieren",
+ "cancel": "Abbrechen",
+ "subject": "Betreff",
+ "message": "Nachricht",
+ "send": "Senden",
+ "required": "erforderlich",
+ "save": "Speichern",
+ "retry": "Noch einmal versuchen",
+ "next": "Nächstes"
+ },
+ "app": {
+ "file_sharing": " Dateifreigabe",
+ "folders": "Ordner",
+ "video": "Video",
+ "contact_details": "Kontaktdaten",
+ "results_search": "Suchergebnisse",
+ "my_space": "Mein Raum"
+ },
+ "login_s": {
+ "install": "Installieren",
+ "username": "Benutzername",
+ "password": "Passwort"
+ },
+ "home_s": {
+ "1": "Kataloge",
+ "2": "Datenblätter",
+ "3": "Andere"
+ },
+ "search_s": {
+ "no_result": "Keine Ergebnisse"
+ },
+ "sync_s": {
+ "download": "Download",
+ "update_completed": "Update abgeschlossen",
+ "updating_docs": "Dokumentaktualisierung",
+ "updating_folders": "Dateien aktualisieren",
+ "sending_flyers": "Brochure senden",
+ "updating_contacts": "Kontakt aktualisieren",
+ "user_install_completed": "Benutzerinstallation abgeschlossen",
+ "updating_users": "Benutzer-Update",
+ "user_install": "Benutzerinstallation",
+ "start_sync": "Synchronisation starten",
+ "new_app_version": "Eine neue Version der Anwendung ist verfügbar!",
+ "open_store": "Öffnen Sie den App Store"
+ },
+ "contact_s": {
+ "contact_transferred": "Kontakt weiterleiten",
+ "contact_will_be_sent_after_sync": "Der Kontakt wird nach der Synchronisation gesendet",
+ "confirm_contact_delete": "Möchten Sie diesen Kontakt wirklich löschen?",
+ "contact_deleted": "Kontakt gelöscht",
+ "contact_added": "Kontakt hinzugefügt",
+ "send_flyer": "Senden Sie eine Broschüre",
+ "select_contact": "Wählen Sie den Kontakt",
+ "transfer": "weiterleiten",
+ "contact_list": "Kontaktliste",
+ "documents_list": "Liste der Dokumente"
+ },
+ "contact_details_s": {
+ "visits": "Besuche",
+ "notes": "Notes",
+ "contact_saved": "Kontakt gespeichert",
+ "confirm_visit_delete": "Sind Sie sicher, dass Sie diesen Besuch löschen möchten?",
+ "qrcode_not_vcard_format": "Dieser QRCode ist nicht im vCard-Format",
+ "customer": "Kunde",
+ "prospect": "potenzieller Kunde",
+ "other": "Andere",
+ "description": "Beschreibung",
+ "no_visits": "Kein Besuch",
+ "visit_name": "Besuchsname"
+ },
+ "error": {
+ "error": "Fehler ",
+ "tablet_not_connected_internet": "Das Tablet ist nicht mit dem Internet verbunden.",
+ "tablet_not_synced_last_user": "Das Tablet wurde vom vorherigen Benutzer nicht synchronisiert",
+ "incorrect_credentials": "Falsche Anmeldeinformationen",
+ "must_fill_login_fields": "Sie müssen die Verbindungsfelder ausfüllen",
+ "invalid_email_address": "Ungültige E-Mail-Adresse",
+ "error_has_occurred": "Ein Fehler ist aufgetreten",
+ "must_be_connected_internet_sync": "Sie müssen mit dem Internet verbunden sein, um die Synchronisierung zu starten",
+ "incomplete_form": "Unvollständiges Formular",
+ "fill_all_fields_and_1_doc_min": "Bitte füllen Sie alle Felder, mindestens 1 Dokument und eine gültige E-Mail-Adresse aus.",
+ "fill_other_field": "Füllen Sie das Beschreibungsfeld für: Andere",
+ "specify_type_contact": "Bitte geben Sie die Art des Kontakts an",
+ "invalid_name": "ungültiger Name",
+ "invalid_firstname": "ungültiger Vorname",
+ "camera_permission_denied": "Kameraerlaubnis verweigert",
+ "pending_camera_permission": "Warten auf Kameraerlaubnis"
+ },
+ "custom": {
+ "select-1": {
+ "title": "Type de commerce",
+ "c1": "Commerce de détail",
+ "c2": "Home Service / Freezer Center",
+ "c3": "Restauration service à table",
+ "c4": "Fast food et snacking",
+ "c5": "Cafétaria / Self Service",
+ "c6": "Société de restauration",
+ "c7": "Restauration gré à gré / cuisine Centrale",
+ "c8": "Distributeur indépendant",
+ "c9": "Boulangerie pâtisserie",
+ "c10": "PAI",
+ "c11": "Destockeur",
+ "c12": "Commerce Halal",
+ "c13": "Restauration d'hébergement",
+ "c14": "Centrale restauration commerciale",
+ "c15": "Restauration évenementielle et parc",
+ "c16": "Restauration de transport",
+ "c17": "Marché public",
+ "c18": "Cash",
+ "c19": "Distributeur national",
+ "c20": "Rungis",
+ "c21": "Abbatoir",
+ "c22": "Autre"
+ },
+ "select-2": {
+ "title": "Site",
+ "c1": "Celvia Dinde",
+ "c2": "Celvia Elaborés",
+ "c3": "Celvia Sérent",
+ "c4": "Procanar",
+ "c5": "Celtys",
+ "c6": "Farmor",
+ "c7": "Robichon",
+ "c8": "Les Volailles de Kerana",
+ "c9": "Bosher Volaille",
+ "c10": "LDC Bretagne",
+ "c11": "Doux Farmor",
+ "c12": "Favreau"
+ },
+ "interest": {
+ "p1": "Poulet",
+ "p2": "Dinde",
+ "p3": "Canard",
+ "p4": "Autre"
+ }
+ }
+}
diff --git a/ComApp/src/Locales/en.json b/ComApp/src/Locales/en.json
new file mode 100644
index 0000000..7d61405
--- /dev/null
+++ b/ComApp/src/Locales/en.json
@@ -0,0 +1,157 @@
+{
+ "global": {
+ "yes": "Yes",
+ "no": "No",
+ "home": "Home",
+ "search": "Search",
+ "login": "Login",
+ "email": "Email",
+ "message_sent": "Message Sent",
+ "message_will_be_sent_after_sync": "Message will be send after synchronization",
+ "warning": "Warning",
+ "synchronization": "Synchronization",
+ "videos": "Videos",
+ "contacts": "Contacts",
+ "confirmation": "Confirmation",
+ "name": "Lastname",
+ "firstname": "Firstname",
+ "address": "Adress",
+ "phone": "Phone number",
+ "company": "Company",
+ "add": "Add",
+ "cancel": "Cancel",
+ "subject": "Object",
+ "message": "Message",
+ "send": "Send",
+ "required": "required",
+ "save": "Save",
+ "retry": "Retry",
+ "next": "Next"
+ },
+ "app": {
+ "file_sharing": "File Sharing",
+ "folders": "Folders",
+ "video": "Video",
+ "contact_details": "Contact information",
+ "results_search": "Search results",
+ "my_space": "Personal Dashboard"
+ },
+ "login_s": {
+ "install": "Install",
+ "username": "Username",
+ "password": "Password"
+ },
+ "home_s": {
+ "1": "Catalogs",
+ "2": "Data sheets",
+ "3": "Others"
+ },
+ "search_s": {
+ "no_result": "No Result"
+ },
+ "sync_s": {
+ "download": "Download",
+ "update_completed": "Update Done",
+ "updating_docs": "Updating documents",
+ "updating_folders": "Updating folders",
+ "sending_flyers": "Sending documents",
+ "updating_contacts": "Updating contacts",
+ "user_install_completed": "User install completed",
+ "updating_users": "Updating Users",
+ "user_install": "Installing Users",
+ "start_sync": "START SYNCHRONIZATION",
+ "new_app_version": "A new version of the application is available!",
+ "open_store": "Open the store"
+ },
+ "contact_s": {
+ "contact_transferred": "Contact Transferred",
+ "contact_will_be_sent_after_sync": "The contact will be sent after synchronization",
+ "confirm_contact_delete": "Are you sure to delete this contact ?",
+ "contact_deleted": "Contact deleted",
+ "contact_added": "Contact added",
+ "send_flyer": "Send a document",
+ "select_contact": "Select the contact",
+ "transfer": "Transfer",
+ "contact_list": "Contact List",
+ "documents_list": "Documents List"
+ },
+ "contact_details_s": {
+ "visits": "Visits",
+ "notes": "Notes",
+ "contact_saved": "Contact saved",
+ "confirm_visit_delete": "are you sure to delete this visit ?",
+ "qrcode_not_vcard_format": "This QRCode is not a VCard",
+ "customer": "Customer",
+ "prospect": "Prospect",
+ "other": "Other",
+ "description": "Description",
+ "no_visits": "No visits available",
+ "visit_name": "visits name"
+ },
+ "error": {
+ "error": "Error",
+ "tablet_not_connected_internet": "The tablet is not connected to Internet",
+ "tablet_not_synced_last_user": "The previous user didn't synchronised the tablet",
+ "incorrect_credentials": "Incorrect Credentials",
+ "must_fill_login_fields": "You must fill the login fields.",
+ "invalid_email_address": "Invalid E-mail adress",
+ "error_has_occurred": "An error occured",
+ "must_be_connected_internet_sync": "You must be connected to internet to be able to synchronize",
+ "incomplete_form": "Incomplete Form",
+ "fill_all_fields_and_1_doc_min": "Please fill up all the fields, at least one document and a valid e-mail adress",
+ "fill_other_field": "You must fill the description field for : Other",
+ "specify_type_contact": "You must specify the contact type",
+ "invalid_name": "Invalid lastname",
+ "invalid_firstname": "Invalid firstname",
+ "camera_permission_denied": "Camera Permission Denied",
+ "pending_camera_permission": "Waiting for the camera permission"
+ },
+ "custom": {
+ "select-1": {
+ "title": "Type de commerce",
+ "c1": "Commerce de détail",
+ "c2": "Home Service / Freezer Center",
+ "c3": "Restauration service à table",
+ "c4": "Fast food et snacking",
+ "c5": "Cafétaria / Self Service",
+ "c6": "Société de restauration",
+ "c7": "Restauration gré à gré / cuisine Centrale",
+ "c8": "Distributeur indépendant",
+ "c9": "Boulangerie pâtisserie",
+ "c10": "PAI",
+ "c11": "Destockeur",
+ "c12": "Commerce Halal",
+ "c13": "Restauration d'hébergement",
+ "c14": "Centrale restauration commerciale",
+ "c15": "Restauration évenementielle et parc",
+ "c16": "Restauration de transport",
+ "c17": "Marché public",
+ "c18": "Cash",
+ "c19": "Distributeur national",
+ "c20": "Rungis",
+ "c21": "Abbatoir",
+ "c22": "Autre"
+ },
+ "select-2": {
+ "title": "Site",
+ "c1": "Celvia Dinde",
+ "c2": "Celvia Elaborés",
+ "c3": "Celvia Sérent",
+ "c4": "Procanar",
+ "c5": "Celtys",
+ "c6": "Farmor",
+ "c7": "Robichon",
+ "c8": "Les Volailles de Kerana",
+ "c9": "Bosher Volaille",
+ "c10": "LDC Bretagne",
+ "c11": "Doux Farmor",
+ "c12": "Favreau"
+ },
+ "interest": {
+ "p1": "Poulet",
+ "p2": "Dinde",
+ "p3": "Canard",
+ "p4": "Autre"
+ }
+ }
+}
diff --git a/ComApp/src/Locales/fr.json b/ComApp/src/Locales/fr.json
new file mode 100644
index 0000000..5596382
--- /dev/null
+++ b/ComApp/src/Locales/fr.json
@@ -0,0 +1,157 @@
+{
+ "global": {
+ "yes": "Oui",
+ "no": "Non",
+ "home": "Accueil",
+ "search": "Recherche",
+ "login": "Connexion",
+ "email": "Email",
+ "message_sent": "Message envoyé",
+ "message_will_be_sent_after_sync": "Le message sera envoyé après synchronisation",
+ "warning": "Attention",
+ "synchronization": "Synchronisation",
+ "videos": "Vidéos",
+ "contacts": "Contacts",
+ "confirmation": "Confirmation",
+ "name": "Nom",
+ "firstname": "Prénom",
+ "address": "Adresse",
+ "phone": "Téléphone",
+ "company": "Société",
+ "add": "Ajouter",
+ "cancel": "Annuler",
+ "subject": "Objet",
+ "message": "Message",
+ "send": "Envoyer",
+ "required": "requis",
+ "save": "Enregistrer",
+ "retry": "Réessayer",
+ "next": "Suivant"
+ },
+ "app": {
+ "file_sharing": "Partage de fichier",
+ "folders": "Dossiers",
+ "video": "Vidéo",
+ "contact_details": "Détails du contact",
+ "results_search": "Résultat(s) de votre recherche",
+ "my_space": "Mon espace"
+ },
+ "login_s": {
+ "install": "Installer",
+ "username": "Identifiant",
+ "password": "Mot de passe"
+ },
+ "home_s": {
+ "1": "Catalogues",
+ "2": "Fiches techniques",
+ "3": "Autres"
+ },
+ "search_s": {
+ "no_result": "Aucun résultat"
+ },
+ "sync_s": {
+ "download": "Téléchargement",
+ "update_completed": "Mise à jour effectuée",
+ "updating_docs": "Mise à jour des documents",
+ "updating_folders": "Mise à jour des dossiers",
+ "sending_flyers": "Envoi des brochures",
+ "updating_contacts": "Mise à jour des contacts",
+ "user_install_completed": "Installation des utilisateurs effectuée",
+ "updating_users": "Mise à jour des utilisateurs",
+ "user_install": "Installation des utilisateurs",
+ "start_sync": "LANCER LA SYNCHRONISATION",
+ "new_app_version": "Une nouvelle version de l'application est disponible !",
+ "open_store": "Ouvrir le store"
+ },
+ "contact_s": {
+ "contact_transferred": "Contact transféré",
+ "contact_will_be_sent_after_sync": "Le contact sera envoyé après synchronisation",
+ "confirm_contact_delete": "Êtes-vous sûr de vouloir supprimer ce contact ?",
+ "contact_deleted": "Contact supprimé",
+ "contact_added": "Contact ajouté",
+ "send_flyer": "Envoyer une brochure",
+ "select_contact": "Sélectionnez le contact",
+ "transfer": "Transférer",
+ "contact_list": "Liste des contacts",
+ "documents_list": "Liste des documents"
+ },
+ "contact_details_s": {
+ "visits": "Visites",
+ "notes": "Notes",
+ "contact_saved": "Contact enregistré",
+ "confirm_visit_delete": "Êtes-vous sûr de vouloir supprimer cette visite ?",
+ "qrcode_not_vcard_format": "Ce QRCode n'est pas au format vCard",
+ "customer": "Client",
+ "prospect": "Prospect",
+ "other": "Autre",
+ "description": "Description",
+ "no_visits": "Aucune visites",
+ "visit_name": "Nom de la visite"
+ },
+ "error": {
+ "error": "Erreur",
+ "tablet_not_connected_internet": "La tablette n'est pas connectée à internet.",
+ "tablet_not_synced_last_user": "La tablette n'as pas été synchronisée par l'utilisateur précédent.",
+ "incorrect_credentials": "Identifiants incorrect.",
+ "must_fill_login_fields": "Vous devez-remplir les champs de connexion.",
+ "invalid_email_address": "Adresse email invalide",
+ "error_has_occurred": "Une erreur est survenue",
+ "must_be_connected_internet_sync": "Vous devez être connecté à internet pour lancer la synchronisation",
+ "incomplete_form": "Formulaire incomplet",
+ "fill_all_fields_and_1_doc_min": "Veuillez remplir tous les champs, 1 document minimum et une adresse email valide",
+ "fill_other_field": "Vous devez remplir le champs description pour : Autre",
+ "specify_type_contact": "Vous devez préciser le type de contact",
+ "invalid_name": "Nom invalide",
+ "invalid_firstname": "Prénom invalide",
+ "camera_permission_denied": "Permission caméra refusée",
+ "pending_camera_permission": "En attente de permission caméra"
+ },
+ "custom": {
+ "select-1": {
+ "title": "Type de commerce",
+ "c1": "Commerce de détail",
+ "c2": "Home Service / Freezer Center",
+ "c3": "Restauration service à table",
+ "c4": "Fast food et snacking",
+ "c5": "Cafétaria / Self Service",
+ "c6": "Société de restauration",
+ "c7": "Restauration gré à gré / cuisine Centrale",
+ "c8": "Distributeur indépendant",
+ "c9": "Boulangerie pâtisserie",
+ "c10": "PAI",
+ "c11": "Destockeur",
+ "c12": "Commerce Halal",
+ "c13": "Restauration d'hébergement",
+ "c14": "Centrale restauration commerciale",
+ "c15": "Restauration évenementielle et parc",
+ "c16": "Restauration de transport",
+ "c17": "Marché public",
+ "c18": "Cash",
+ "c19": "Distributeur national",
+ "c20": "Rungis",
+ "c21": "Abbatoir",
+ "c22": "Autre"
+ },
+ "select-2": {
+ "title": "Site",
+ "c1": "Celvia Dinde",
+ "c2": "Celvia Elaborés",
+ "c3": "Celvia Sérent",
+ "c4": "Procanar",
+ "c5": "Celtys",
+ "c6": "Farmor",
+ "c7": "Robichon",
+ "c8": "Les Volailles de Kerana",
+ "c9": "Bosher Volaille",
+ "c10": "LDC Bretagne",
+ "c11": "Doux Farmor",
+ "c12": "Favreau"
+ },
+ "interest": {
+ "p1": "Poulet",
+ "p2": "Dinde",
+ "p3": "Canard",
+ "p4": "Autre"
+ }
+ }
+}
diff --git a/ComApp/src/Models/Common.js b/ComApp/src/Models/Common.js
new file mode 100644
index 0000000..3d8ab03
--- /dev/null
+++ b/ComApp/src/Models/Common.js
@@ -0,0 +1,64 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class CommonModel {
+ constructor() {
+ this.schema = 'Common';
+ }
+
+ add(data) {
+ realm.write(() => {
+ if (data.id === undefined) {
+ let id: any = realm.objects(this.schema).max('id');
+ data.id = id === undefined ? 1 : id + 1;
+ }
+ try {
+ realm.create(this.schema, data, 'never');
+ } catch (e) {
+ console.log('db common add', e);
+ }
+ });
+ }
+
+ update(data) {
+ let tmp = this.get(data.key);
+ if (tmp === undefined) {
+ this.add(data);
+ } else {
+ if (data.id === undefined) {
+ data.id = tmp.id;
+ }
+ realm.write(() => {
+ realm.create(this.schema, data, 'modified');
+ });
+ }
+ }
+
+ get(key) {
+ let tmp = realm.objects(this.schema);
+ if (tmp.length === 0) {
+ return false;
+ } else {
+ tmp = tmp.filtered('key = "' + key + '"')[0];
+ }
+ return tmp;
+ }
+
+ getAll() {
+ let tmp = realm.objects(this.schema);
+ return tmp;
+ }
+
+ delete(key) {
+ const tmp = this.get(key);
+ if (typeof tmp === 'object') {
+ realm.write(() => {
+ realm.delete(tmp);
+ });
+ }
+ }
+}
+
+export default new CommonModel();
diff --git a/ComApp/src/Models/Contacts.js b/ComApp/src/Models/Contacts.js
new file mode 100644
index 0000000..8cb1243
--- /dev/null
+++ b/ComApp/src/Models/Contacts.js
@@ -0,0 +1,60 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class ContactsModel {
+ constructor() {
+ this.schema = 'Contacts';
+ }
+
+ add(contact) {
+ contact.uniq_id = contact.id;
+ contact.id = undefined;
+ realm.write(() => {
+ if (contact.id === undefined) {
+ let id: any = realm.objects(this.schema).max('id');
+ contact.id = id === undefined ? 1 : id + 1;
+ }
+ realm.create(this.schema, contact);
+ });
+ }
+
+ update(contact) {
+ realm.write(() => {
+ realm.create(this.schema, contact, true);
+ });
+ }
+
+ get(id) {
+ let tmp = realm.objects(this.schema).filtered('id = ' + id)[0];
+ return tmp;
+ }
+
+ getAll() {
+ let tmp = realm.objects(this.schema);
+ return tmp;
+ }
+
+ getByUser(userId) {
+ let tmp = realm.objects(this.schema);
+ tmp = tmp.filtered('to_delete = false AND user_id = ' + userId);
+ return tmp;
+ }
+
+ delete(id) {
+ const tmp = this.get(id);
+ realm.write(() => {
+ tmp.to_delete = true;
+ // HARD DELETE - > realm.delete(tmp);
+ });
+ }
+
+ clear() {
+ realm.write(() => {
+ realm.delete(this.getAll());
+ });
+ }
+}
+
+export default new ContactsModel();
diff --git a/ComApp/src/Models/Directories.js b/ComApp/src/Models/Directories.js
new file mode 100644
index 0000000..85d7b99
--- /dev/null
+++ b/ComApp/src/Models/Directories.js
@@ -0,0 +1,104 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class DirectoriesModel {
+ constructor() {
+ this.schema = 'Directories';
+ }
+
+ add(directory) {
+ realm.write(() => {
+ realm.create(this.schema, directory);
+ });
+ }
+
+ update(directory) {
+ realm.write(() => {
+ realm.create(this.schema, directory, true);
+ });
+ }
+
+ get(id) {
+ let tmp = realm.objects(this.schema).filtered('id = ' + id)[0];
+ return tmp;
+ }
+
+ getAll(role, authorizedFolders, format, lang) {
+ if (authorizedFolders !== null && authorizedFolders !== undefined) {
+ authorizedFolders = JSON.parse(authorizedFolders);
+ }
+ let tmp = realm.objects(this.schema);
+ if (role === 'USER') {
+ if (authorizedFolders.length > 0) {
+ tmp = tmp.filtered(
+ 'needAuthorization = 0 OR ' +
+ authorizedFolders
+ .map(folder => ' id = "' + folder.id + '"')
+ .join(' OR '),
+ );
+ } else {
+ tmp = tmp.filtered(
+ 'needAuthorization = 0'
+ );
+ }
+ }
+ if (format) {
+ return this._recursiveDir(tmp, lang);
+ }
+ return tmp;
+ }
+
+ clear() {
+ realm.write(() => {
+ realm.delete(this.getAll('SYNC', null, false));
+ });
+ }
+
+ _recursiveDir(list, lang, parent = null) {
+ let root = list.filter(e => e.parent === parent);
+ let tmp = [];
+ for (let i = 0; i < root.length; i++) {
+ let childs = this._recursiveDir(list, lang, root[i].id);
+ let name = JSON.parse(root[i].name);
+ tmp[i] = {};
+ tmp[i].id = root[i].id;
+ tmp[i].name = this.getI18nName(lang, name);
+ tmp[i].items = [];
+ for (let j = 0; j < childs.length; j++) {
+ childs[j].value = childs[j].id;
+ tmp[i].items.push(childs[j]);
+ }
+ if (!tmp[i].items.length) {
+ delete tmp[i].items;
+ }
+ }
+ return tmp;
+ }
+
+ getI18nName(lang, name) {
+ let tmp = undefined;
+ switch (lang) {
+ case "fr":
+ tmp = name.fr;
+ break;
+ case "en":
+ tmp = name.en;
+ break;
+ case "de":
+ tmp = name.de;
+ break;
+ case "es":
+ tmp = name.es;
+ break;
+ }
+ if (tmp === undefined) {
+ tmp = name.fr;
+ }
+ return tmp;
+ }
+
+}
+
+export default new DirectoriesModel();
diff --git a/ComApp/src/Models/Documents.js b/ComApp/src/Models/Documents.js
new file mode 100644
index 0000000..18ce430
--- /dev/null
+++ b/ComApp/src/Models/Documents.js
@@ -0,0 +1,68 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class DocumentsModel {
+ constructor() {
+ this.schema = 'Documents';
+ }
+
+ add(document) {
+ realm.write(() => {
+ realm.create(this.schema, document, 'never');
+ });
+ }
+
+ update(document, updatedAtChange = false) {
+ let tmp = this.get(document.id);
+ if (tmp === undefined) {
+ this.add(document);
+ } else {
+ realm.write(() => {
+ if (updatedAtChange === false) {
+ realm.create(this.schema, {
+ id: tmp.id,
+ uuid: document.uuid,
+ title: document.title,
+ type: document.type,
+ path: document.path,
+ directory: document.directory
+ }, 'modified');
+ } else {
+ realm.create(this.schema, document, 'modified');
+ }
+ });
+ }
+ }
+
+ get(id) {
+ let tmp = realm.objects(this.schema).filtered('id = ' + id)[0];
+ return tmp;
+ }
+
+ getAll(authorizedDirs = false) {
+ let tmp = realm.objects(this.schema);
+ if (authorizedDirs) {
+ authorizedDirs = authorizedDirs.map(item => item.id);
+ tmp = tmp.filter(doc => authorizedDirs.includes(doc.directory));
+ }
+ return tmp;
+ }
+
+ clear(docs, toDelete = []) {
+ const localDocs = this.getAll();
+ localDocs.forEach(doc => {
+ let tmp = docs.filter(item => item.uuid === doc.uuid);
+ if (tmp.length === 0) {
+ toDelete.push({uuid: doc.uuid, type: doc.type});
+ realm.write(() => {
+ realm.delete(doc);
+ });
+ }
+ });
+ return toDelete;
+ }
+}
+
+export default new DocumentsModel();
diff --git a/ComApp/src/Models/SendBrochure.js b/ComApp/src/Models/SendBrochure.js
new file mode 100644
index 0000000..fab3920
--- /dev/null
+++ b/ComApp/src/Models/SendBrochure.js
@@ -0,0 +1,49 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class SendBrochureModel {
+ constructor() {
+ this.schema = 'SendBrochure';
+ }
+
+ add(data) {
+ realm.write(() => {
+ if (data.id === undefined) {
+ let id: any = realm.objects(this.schema).max('id');
+ data.id = id === undefined ? 1 : id + 1;
+ }
+ realm.create(this.schema, data);
+ });
+ }
+
+ update(data) {
+ realm.write(() => {
+ realm.create(this.schema, data, true);
+ });
+ }
+
+ get(id) {
+ let tmp = realm.objects(this.schema);
+ if (tmp.length === 0) {
+ return false;
+ } else {
+ tmp = tmp.filtered('id = ' + id)[0];
+ }
+ return tmp;
+ }
+
+ getAll() {
+ let tmp = realm.objects(this.schema);
+ return tmp;
+ }
+
+ clear() {
+ realm.write(() => {
+ realm.delete(this.getAll());
+ });
+ }
+}
+
+export default new SendBrochureModel();
diff --git a/ComApp/src/Models/Users.js b/ComApp/src/Models/Users.js
new file mode 100644
index 0000000..1d816ed
--- /dev/null
+++ b/ComApp/src/Models/Users.js
@@ -0,0 +1,40 @@
+import Database from '../Database';
+
+const Realm = require('realm');
+let realm = new Realm(Database);
+
+class UsersModel {
+ constructor() {
+ this.schema = 'Users';
+ }
+
+ add(user) {
+ realm.write(() => {
+ realm.create(this.schema, user);
+ });
+ }
+
+ update(user) {
+ realm.write(() => {
+ realm.create(this.schema, user, true);
+ });
+ }
+
+ get(id) {
+ let tmp = realm.objects(this.schema).filtered('id = ' + id)[0];
+ return tmp;
+ }
+
+ getAll() {
+ let tmp = realm.objects(this.schema);
+ return tmp;
+ }
+
+ clear() {
+ realm.write(() => {
+ realm.delete(this.getAll());
+ });
+ }
+}
+
+export default new UsersModel();
diff --git a/ComApp/src/Views/Contact.js b/ComApp/src/Views/Contact.js
new file mode 100644
index 0000000..aa62230
--- /dev/null
+++ b/ComApp/src/Views/Contact.js
@@ -0,0 +1,606 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ Button,
+ FlatList,
+ Text,
+ Dimensions,
+ TouchableOpacity,
+ Alert,
+ TextInput,
+} from 'react-native';
+import Icon from 'react-native-vector-icons/FontAwesome';
+import { connect, ReactReduxContext } from 'react-redux';
+import { showMessage } from 'react-native-flash-message';
+import { Overlay } from '@rneui/themed';
+import Helpers from "../Controllers/Helpers";
+import i18n from "../I18n";
+import { Picker } from '@react-native-picker/picker';
+
+function mapStateToProps(state) {
+ return {
+ users: state.users,
+ contacts: state.contacts,
+ documents: state.documents,
+ currentUser: state.currentUser,
+ };
+}
+
+class ContactScreen extends React.Component {
+ state = {
+ contacts: [],
+ sortType: 'asc',
+ sortField: 'name',
+ searchTxt: '',
+ showTransferContact: false,
+ selectedTransferUser: 0,
+ contactToTransfer: {},
+ showSendBrochure: false,
+ selectedDocument: -1,
+ sendDocumentList: [],
+ selectedSendBrochureEmail: -1,
+ tmpSendBrochureEmail: '',
+ tmpSendBrochureSubject: '',
+ tmpSendBrochureMessage: '',
+ sortedDocuments: [],
+ sortedEmails: [],
+ };
+
+ sendBrochure() {
+ if (
+ this.state.tmpSendBrochureEmail !== '' &&
+ this.state.tmpSendBrochureSubject !== '' &&
+ this.state.tmpSendBrochureMessage !== '' &&
+ this.state.sendDocumentList.length > 0 &&
+ Helpers.isEmailValid(this.state.tmpSendBrochureEmail)
+ ) {
+ const email = {
+ email: this.state.tmpSendBrochureEmail,
+ subject: this.state.tmpSendBrochureSubject,
+ message: this.state.tmpSendBrochureMessage,
+ documents: JSON.stringify(this.state.sendDocumentList),
+ user_id: this.props.currentUser.id
+ };
+ this.props.dispatch({ type: 'SEND_BROCHURE', data: email });
+ this.setState({
+ showSendBrochure: false,
+ tmpSendBrochureEmail: '',
+ tmpSendBrochureSubject: '',
+ tmpSendBrochureMessage: '',
+ selectedSendBrochureEmail: -1,
+ selectedDocument: -1,
+ sendDocumentList: [],
+ });
+ showMessage({
+ message: i18n.t('global.message_sent') + '.',
+ description: i18n.t('global.message_will_be_sent_after_sync') + '.',
+ type: 'success',
+ duration: 1500,
+ });
+ } else {
+ Alert.alert(
+ i18n.t('error.incomplete_form'),
+ i18n.t('error.fill_all_fields_and_1_doc_min') + '.',
+ );
+ }
+ }
+
+ transferContact() {
+ const nextUser = this.props.users.filter(user => user.id !== this.props.currentUser.id)[this.state.selectedTransferUser];
+ const transferContact = this.state.contactToTransfer;
+ const contact = {
+ id: transferContact.id,
+ email: transferContact.email,
+ type: transferContact.type,
+ other: transferContact.other,
+ network: transferContact.network,
+ name: transferContact.name,
+ firstname: transferContact.firstname,
+ office: transferContact.office,
+ company: transferContact.company,
+ address: transferContact.address,
+ phone: transferContact.phone,
+ products: transferContact.products,
+ notes: transferContact.note,
+ visits: transferContact.visits,
+ to_delete: false,
+ synced: false,
+ user_id: nextUser.id,
+ };
+ this.props.dispatch({ type: 'UPDATE_CONTACT', data: contact });
+ this.setState({ showTransferContact: false });
+ showMessage({
+ message: i18n.t('contact_s.contact_transferred') + '.',
+ description: i18n.t('contact_s.contact_will_be_sent_after_sync') + '.',
+ type: 'success',
+ duration: 1500,
+ });
+ }
+
+ deleteContact(id) {
+ Alert.alert(
+ i18n.t('global.confirmation'),
+ i18n.t('contact_s.confirm_contact_delete'),
+ [
+ {
+ text: i18n.t('global.no'),
+ onPress: () => console.log('Cancel Pressed'),
+ style: 'cancel',
+ },
+ {
+ text: i18n.t('global.yes'),
+ onPress: () => {
+ this.setState({ contacts: [], sortedEmails: [] });
+ this.props.dispatch({ type: 'DELETE_CONTACT', data: id });
+ showMessage({
+ message: i18n.t('contact_s.contact_deleted') + '.',
+ type: 'success',
+ duration: 500,
+ });
+ },
+ },
+ ],
+ { cancelable: false },
+ );
+ }
+
+ Item(item) {
+ return (
+
+
+ this.props.navigation.navigate('ContactDetailsScreen', {
+ action: 'edit',
+ contactId: item.id,
+ })
+ }>
+
+ {item.name.toUpperCase()} {item.firstname}
+
+
+ {item.company}
+ {item.email}
+
+ {
+ this.deleteContact(item.id);
+ }}>
+
+
+ {
+ this.setState({
+ showTransferContact: true,
+ contactToTransfer: item,
+ });
+ }}>
+
+
+
+
+ );
+ }
+
+ sortContacts(field) {
+ let sort;
+ if (field !== this.state.sortField) {
+ sort = 'asc';
+ } else {
+ if (this.state.sortType === 'asc') {
+ sort = 'desc';
+ } else {
+ sort = 'asc';
+ }
+ }
+ this.setState({ contacts: this.state.contacts.sort(Helpers.compareValues(field, sort)), sortType: sort, sortField: field });
+ }
+
+ HeaderItem() {
+ return (
+
+ {
+ this.sortContacts('name');
+ }}>
+ {i18n.t('global.name')}
+
+ {
+ this.sortContacts('company');
+ }}>
+ {i18n.t('global.company')}
+
+ {i18n.t('global.email')}
+
+
+ );
+ }
+
+ handleSearchBtn() {
+ if (this.state.searchTxt.length > 0) {
+ let obj = this.props.contacts;
+ let array = Object.keys(obj).map(function (key) { return obj[key]; });
+ this.setState({ contacts: array.sort(Helpers.compareValues('name', 'asc')), sortType: 'asc', searchTxt: '' });
+ }
+ }
+ handleSearchTxt(text) {
+ this.setState({ searchTxt: text });
+ if (text.length > 0) {
+ let tmpContactsName = this.props.contacts.filter(item => item.name.toLowerCase().indexOf(text.toLowerCase()) >= 0);
+ let tmpContactsCompany = this.props.contacts.filter(item => item.company.toLowerCase().indexOf(text.toLowerCase()) >= 0);
+ let resSearch = Object.assign(tmpContactsName, tmpContactsCompany);
+ this.setState({ contacts: resSearch.sort(Helpers.compareValues('name', 'asc')), sortType: 'asc' });
+ } else {
+ let obj = this.props.contacts;
+ let array = Object.keys(obj).map(function (key) { return obj[key]; });
+ this.setState({ contacts: array.sort(Helpers.compareValues('name', 'asc')), sortType: 'asc' });
+ }
+ }
+
+ componentDidMount() {
+ this.init();
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (prevProps.contacts !== this.props.contacts) {
+ this.init();
+ }
+ }
+
+ init() {
+ let objContacts = this.props.contacts;
+ let objDocs = this.props.documents;
+ let arrayContacts = Object.keys(objContacts).map(function (key) { return objContacts[key]; });
+ let arrayEmails = Object.keys(objContacts).map(function (key) { return objContacts[key]; });
+ let arrayDocs = Object.keys(objDocs).map(function (key) { return objDocs[key]; });
+
+ this.setState({
+ contacts: arrayContacts.sort(Helpers.compareValues('name', 'asc')),
+ sortedDocuments: arrayDocs.sort(Helpers.compareValues('title', 'asc')),
+ sortedEmails: arrayEmails.sort(Helpers.compareValues('email', 'asc')),
+ sortType: 'asc',
+ sortField: 'name',
+ });
+ }
+
+ render() {
+ const { contactAdded } = this.props.route.params;
+ if (contactAdded === true) {
+ this.props.navigation.setParams({ contactAdded: false });
+ showMessage({
+ message: i18n.t('contact_s.contact_added') + '.',
+ type: 'success',
+ duration: 500,
+ });
+ }
+ return (
+
+
+
+
+ this.Item(item)}
+ ListHeaderComponent={this.HeaderItem.bind(this)}
+ keyExtractor={item => item.id.toString()}
+ />
+
+
+
+ {i18n.t('contact_s.select_contact')} :
+ {
+ this.setState({ selectedTransferUser: itemIndex });
+ }}>
+ {this.props.users.filter(user => user.id !== this.props.currentUser.id).map((item, index) => {
+ return ;
+ })}
+
+
+ this.setState({ showTransferContact: false })}
+ />
+ this.transferContact()}
+ />
+
+
+
+
+
+
+
+
+ this.setState({ tmpSendBrochureEmail: text })
+ }
+ />
+ {
+ if (itemValue !== '-1') {
+ this.setState({
+ tmpSendBrochureEmail: this.state.sortedEmails[itemIndex - 1]
+ .email,
+ });
+ }
+ }}>
+
+ {this.state.sortedEmails.map((item, index) => {
+ return ;
+ })}
+
+
+ this.setState({ tmpSendBrochureSubject: text })
+ }
+ />
+
+ this.setState({ tmpSendBrochureMessage: text })
+ }
+ />
+ {
+ if (
+ itemValue !== '-1' &&
+ this.state.sendDocumentList.length < 8
+ ) {
+ let tmp = this.state.sendDocumentList;
+ const toPush = this.state.sortedDocuments[itemIndex - 1];
+ const test = this.state.sendDocumentList.filter(
+ item => item.id === toPush.id,
+ );
+ if (test.length === 0) {
+ tmp.push(toPush);
+ this.setState({
+ sendDocumentList: tmp,
+ });
+ }
+ }
+ }}>
+
+ {this.state.sortedDocuments.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ {this.state.sendDocumentList.map((item, index) => {
+ return (
+ {
+ let tmp = this.state.sendDocumentList;
+ tmp.splice(this.props.documents[index], 1);
+ this.setState({
+ sendDocumentList: tmp,
+ });
+ }}>
+
+
+ {item.title} ({item.type.toUpperCase()}){' '}
+
+
+
+
+ );
+ })}
+
+
+
+
+ this.setState({
+ showSendBrochure: false,
+ tmpSendBrochureEmail: '',
+ tmpSendBrochureSubject: '',
+ tmpSendBrochureMessage: '',
+ selectedSendBrochureEmail: -1,
+ selectedDocument: -1,
+ sendDocumentList: [],
+ })
+ }
+ />
+ this.sendBrochure()} color={'#ce3029'} />
+
+
+
+
+ );
+ }
+}
+
+const { width } = Dimensions.get('window');
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ item: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ padding: 15,
+ marginHorizontal: 15,
+ },
+ clickable: {
+ textAlign: 'center',
+ fontSize: 18,
+ width: width / 4,
+ color: '#ce3029',
+ fontWeight: 'bold'
+ },
+ company: {
+ textAlign: 'center',
+ fontSize: 15,
+ width: width / 4,
+ },
+ email: {
+ textAlign: 'center',
+ fontSize: 15,
+ width: width / 4,
+ },
+ buttons: {
+ justifyContent: 'center',
+ flexDirection: 'row',
+ fontSize: 15,
+ width: width / 4,
+ },
+ button: {
+ width: 35,
+ height: 35,
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: 2,
+ },
+ txtSendBrochure: {
+ borderWidth: 1,
+ borderColor: 'lightgrey',
+ width: 280,
+ marginBottom: 5,
+ paddingLeft: 10,
+ },
+ textAreaSendBrochure: {
+ borderWidth: 1,
+ borderColor: 'lightgrey',
+ width: 280,
+ height: 150,
+ marginBottom: 5,
+ paddingLeft: 10,
+ textAlignVertical: 'top',
+ },
+ inputSearch: {
+ backgroundColor: 'white',
+ width: 200,
+ paddingLeft: 20
+ },
+ btnSearch: {
+ width: 50,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#ce3029'
+ }
+});
+
+export default connect(mapStateToProps)(ContactScreen);
diff --git a/ComApp/src/Views/ContactDetails.js b/ComApp/src/Views/ContactDetails.js
new file mode 100644
index 0000000..747cf15
--- /dev/null
+++ b/ComApp/src/Views/ContactDetails.js
@@ -0,0 +1,877 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ Button,
+ TextInput,
+ TouchableOpacity,
+ Text,
+ Alert,
+ ScrollView
+} from 'react-native';
+import { CheckBox, ButtonGroup, Overlay } from '@rneui/themed';
+import { connect } from 'react-redux';
+import Icon from 'react-native-vector-icons/FontAwesome';
+import { showMessage } from 'react-native-flash-message';
+import Helpers from "../Controllers/Helpers";
+import i18n from "../I18n";
+import { BarCodeScanner } from 'expo-barcode-scanner';
+import vCard from 'vcard-parser';
+import { Picker } from '@react-native-picker/picker';
+
+function mapStateToProps(state) {
+ return {
+ contacts: state.contacts,
+ currentUser: state.currentUser,
+ };
+}
+
+let date = new Date().getDate();
+if (date < 10) {
+ date = '0' + date.toString();
+}
+let month = new Date().getMonth() + 1;
+if (month < 10) {
+ month = '0' + month.toString();
+}
+const year = new Date().getFullYear();
+
+class ContactDetailsScreen extends React.Component {
+ state = {
+ action: 'add',
+ visits: [],
+ selectedVisit: 0,
+ tmpVisitNote: '',
+ newVisitTitle: date + '/' + month + '/' + year,
+ selectedIndex: 0,
+ showNewVisit: false,
+ tmpID: 0,
+ tmpNote: '',
+ tmpName: '',
+ tmpFirstname: '',
+ tmpEmail: '',
+ tmpCompany: '',
+ tmpAddress: '',
+ tmpPhone: '',
+ tmpSelect1: '',
+ tmpSelect2: '',
+ tmpOther: '',
+ clientChecked: false,
+ prospectChecked: false,
+ otherChecked: false,
+ p1: false,
+ p2: false,
+ p3: false,
+ p4: false,
+ p5: false,
+ p6: false,
+ p7: false,
+ p8: false,
+ p9: false,
+ showQRcodevCard: false,
+ parsedType: null,
+ parsedDatas: [],
+ hasPermission: null,
+ scanned: false,
+ showDynContact: false,
+ parsedName: '',
+ parsedFirstname: '',
+ parsedEmail: '',
+ parsedCompany: '',
+ parsedAddress: '',
+ parsedPhone: ''
+ };
+
+ updateContact() {
+ let error = '';
+ const { action } = this.props.route.params;
+ // On récupère le texte courant
+ if (this.state.visits.length > 0) {
+ this.state.visits[
+ this.state.selectedVisit
+ ].note = this.state.tmpVisitNote;
+ }
+ let type;
+ if (this.state.clientChecked === true) {
+ type = 'client';
+ } else if (this.state.prospectChecked === true) {
+ type = 'prospect';
+ } else if (this.state.otherChecked === true) {
+ type = 'other';
+ if (this.state.tmpOther.length === 0) {
+ error = error.concat(i18n.t('error.fill_other_field') + '.\n');
+ }
+ } else {
+ error = error.concat(i18n.t('error.specify_type_contact') + '.\n');
+ }
+ if (this.state.tmpEmail.length < 5 || !Helpers.isEmailValid(this.state.tmpEmail)) {
+ error = error.concat(i18n.t('error.invalid_email_address') + '.\n');
+ }
+ if (this.state.tmpName.length === 0) {
+ error = error.concat(i18n.t('error.invalid_name') + '.\n');
+ }
+ if (this.state.tmpFirstname.length === 0) {
+ error = error.concat(i18n.t('error.invalid_firstname') + '.\n');
+ }
+
+ if (error.length > 0) {
+ Alert.alert(i18n.t('global.warning') + ' !', error, [
+ { text: 'OK' },
+ ],
+ { cancelable: false },
+ );
+ } else {
+ const products = [
+ this.state.p1,
+ this.state.p2,
+ this.state.p3,
+ this.state.p4,
+ this.state.p5,
+ this.state.p6,
+ this.state.p7,
+ this.state.p8,
+ this.state.p9
+ ];
+ const contact = {
+ id: Helpers.generateUUID(),
+ email: this.state.tmpEmail,
+ type: type,
+ other: this.state.tmpOther,
+ select1: this.state.tmpSelect1,
+ select2: this.state.tmpSelect2,
+ name: this.state.tmpName,
+ firstname: this.state.tmpFirstname,
+ company: this.state.tmpCompany,
+ address: this.state.tmpAddress,
+ phone: this.state.tmpPhone,
+ products: JSON.stringify(products),
+ notes: this.state.tmpNote,
+ visits: JSON.stringify(this.state.visits),
+ to_delete: false,
+ synced: false,
+ };
+ if (action === 'edit') {
+ contact.id = this.state.tmpID;
+ this.props.dispatch({ type: 'UPDATE_CONTACT', data: contact });
+ this.populate();
+ showMessage({
+ message: i18n.t('contact_details_s.contact_saved') + '.',
+ type: 'success',
+ duration: 500,
+ });
+ } else {
+ contact.user_id = this.props.currentUser.id;
+ this.props.dispatch({ type: 'ADD_CONTACT', data: contact });
+ this.props.navigation.navigate('ContactScreen', { contactAdded: true });
+ }
+ }
+ }
+
+ updateContactType(type) {
+ if (type === 'client') {
+ this.setState({
+ clientChecked: true,
+ prospectChecked: false,
+ otherChecked: false,
+ tmpOther: '',
+ });
+ } else if (type === 'prospect') {
+ this.setState({
+ clientChecked: false,
+ prospectChecked: true,
+ otherChecked: false,
+ tmpOther: '',
+ });
+ } else {
+ this.setState({
+ clientChecked: false,
+ prospectChecked: false,
+ otherChecked: true,
+ });
+ }
+ }
+
+ newVisit() {
+ this.state.visits.unshift({ title: this.state.newVisitTitle, note: '' });
+ this.state.visits[this.state.selectedVisit].note = this.state.tmpVisitNote;
+ this.setState({
+ tmpVisitNote: '',
+ visits: this.state.visits,
+ showNewVisit: false,
+ selectedVisit: 0,
+ });
+ }
+
+ deleteVisit() {
+ Alert.alert(
+ i18n.t('global.confirmation'),
+ i18n.t('contact_details_s.confirm_visit_delete'),
+ [
+ {
+ text: i18n.t('global.no'),
+ onPress: () => console.log('Cancel Pressed'),
+ style: 'cancel',
+ },
+ {
+ text: i18n.t('global.yes'),
+ onPress: () => {
+ this.state.visits.splice(this.state.selectedVisit, 1);
+ if (this.state.visits.length === 0) {
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: 0,
+ tmpVisitNote: '',
+ });
+ } else {
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: 0,
+ tmpVisitNote: this.state.visits[0].note,
+ });
+ }
+ },
+ },
+ ],
+ { cancelable: false },
+ );
+ }
+
+ populate() {
+ const { contactId } = this.props.route.params;
+ const contact = this.props.contacts.filter(tmp => tmp.id === contactId)[0];
+ this.setState({
+ tmpID: contact.id,
+ tmpNote: contact.notes,
+ tmpName: contact.name,
+ tmpFirstname: contact.firstname,
+ tmpEmail: contact.email,
+ tmpCompany: contact.company,
+ tmpAddress: contact.address,
+ tmpPhone: contact.phone,
+ tmpSelect1: contact.select1,
+ tmpSelect2: contact.select2,
+ tmpOther: contact.other,
+ tmpProducts: contact.product,
+ });
+ if (contact.type === 'client') {
+ this.setState({ clientChecked: true });
+ } else if (contact.type === 'prospect') {
+ this.setState({ prospectChecked: true });
+ } else {
+ this.setState({ otherChecked: true });
+ }
+ if (contact.visits !== null) {
+ const tmp = JSON.parse(contact.visits);
+ this.setState({ visits: tmp });
+ if (tmp.length > 0) {
+ this.setState({ tmpVisitNote: tmp[this.state.selectedVisit].note });
+ }
+ }
+ if (contact.products !== null) {
+ const tmp = JSON.parse(contact.products);
+ this.setState({
+ p1: tmp[0],
+ p2: tmp[1],
+ p3: tmp[2],
+ p4: tmp[3],
+ p5: tmp[4],
+ p6: tmp[5],
+ p7: tmp[6],
+ p8: tmp[7],
+ p9: tmp[8],
+ p10: tmp[9],
+ });
+ }
+ }
+
+ componentDidMount() {
+ const { action } = this.props.route.params;
+ if (action === 'edit') {
+ this.setState({ action: 'edit' });
+ this.populate();
+ }
+
+ BarCodeScanner.requestPermissionsAsync().then(res => {
+ this.setState({ hasPermission: res.status === 'granted' });
+ });
+
+ }
+
+ render() {
+ const handleBarCodeScanned = ({ type, data }) => {
+ this.setState({ scanned: true });
+ if (data.includes('VCARD')) {
+ let datas = vCard.parse(data);
+ let tmpDatas = [];
+ if (datas['n'] !== undefined) {
+ const nx = Object.assign({ n1: [{ value: datas['n'][0].value[0] }] }, { n2: [{ value: datas['n'][0].value[1] }] });
+ datas = Object.assign(datas, nx);
+ }
+ if (datas['adr'] !== undefined) {
+ let tmp = datas['adr'][0].value;
+ let tmpadr = '';
+ for (let i = 0; i < tmp.length; i++) {
+ if (tmp[i].length > 0) {
+ if (tmpadr.length > 0) {
+ tmpadr = tmpadr + ', ' + tmp[i];
+ } else {
+ tmpadr = tmp[i];
+ }
+ }
+ }
+ datas['adr'] = [{ value: tmpadr }];
+ }
+ Object.keys(datas).map((objectKey, index) => {
+ if (objectKey === 'email' || objectKey === 'adr' || objectKey === 'tel' || objectKey === 'org' || objectKey === 'n1' || objectKey === 'n2') {
+ let value = datas[objectKey][0].value;
+ if (typeof value === 'string') {
+ tmpDatas.push(value);
+ }
+ }
+ });
+ this.setState({ parsedType: 'vcard', parsedDatas: tmpDatas, showQRcodevCard: false, showDynContact: true })
+ } else {
+ Alert.alert(i18n.t('contact_details_s.qrcode_not_vcard_format'));
+ }
+ };
+ return (
+
+
+
+ this.updateContactType('client')}
+ />
+ this.updateContactType('prospect')}
+ />
+ this.updateContactType('other')}
+ />
+ {this.state.otherChecked === true && (
+ this.setState({ tmpOther: text })}
+ />
+ )}
+
+
+ this.setState({ tmpName: text })}
+ />
+ this.setState({ tmpFirstname: text })}
+ />
+
+ this.setState({ tmpEmail: text })}
+ autoCapitalize={'none'}
+ autoCorrect={false}
+ keyboardType={'email-address'}
+ />
+ this.setState({ tmpCompany: text })}
+ />
+ this.setState({ tmpAddress: text })}
+ />
+ this.setState({ tmpPhone: text })}
+ autoCompleteType={'tel'}
+ dataDetectorTypes={'phoneNumber'}
+ keyboardType={'phone-pad'}
+ />
+
+
+
+ {
+ if (itemValue !== '0') {
+ this.setState({ tmpSelect1: itemValue });
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (itemValue !== '0') {
+ this.setState({ tmpSelect2: itemValue });
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ this.setState({ p1: !this.state.p1 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p2: !this.state.p2 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p3: !this.state.p3 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p4: !this.state.p4 })}
+ containerStyle={styles.clearCheckbox}
+ />
+
+
+
+
+ this.setState({ selectedIndex: index })}
+ />
+ {this.state.selectedIndex === 0 && (
+
+
+ {this.state.visits.length > 0 && (
+
+
+ {
+ this.state.visits[
+ this.state.selectedVisit
+ ].note = this.state.tmpVisitNote;
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: itemIndex,
+ tmpVisitNote: this.state.visits[itemIndex].note,
+ });
+ }}>
+ {this.state.visits.map((item, index) => {
+ return ;
+ })}
+
+
+ )}
+ {this.state.visits.length === 0 && (
+
+ {i18n.t('contact_details_s.no_visits')}.
+
+ )}
+
+ this.setState({ showNewVisit: true })}>
+
+
+ {this.state.visits.length > 0 && (
+ this.deleteVisit()}>
+
+
+ )}
+
+
+ {this.state.visits.length > 0 && (
+ this.setState({ tmpVisitNote: text })}
+ />
+ )}
+
+ )}
+ {this.state.selectedIndex === 1 && (
+ this.setState({ tmpNote: text })}
+ />
+ )}
+
+
+ {this.state.action === 'add' && (
+ <>
+ this.setState({ showQRcodevCard: true })} color={'#ce3029'} />
+ >
+ )}
+ this.updateContact()} color={'#ce3029'} />
+
+
+
+
+ {i18n.t('contact_details_s.visit_name')} :
+ this.setState({ newVisitTitle: text })}
+ style={{
+ borderWidth: 1,
+ borderColor: 'lightgrey',
+ width: 300,
+ marginBottom: 15,
+ paddingLeft: 10,
+ }}
+ />
+
+ this.setState({ showNewVisit: false })}
+ color={'#ce3029'}
+ />
+ this.newVisit()} color={'#ce3029'} />
+
+
+
+
+
+ {this.state.hasPermission === true && (
+
+ )}
+ {this.state.hasPermission === false && (
+ {i18n.t('error.camera_permission_denied')}.
+ )}
+ {this.state.hasPermission === null && (
+ {i18n.t('error.pending_camera_permission')}.
+ )}
+
+ this.setState({ showQRcodevCard: false, scanned: false })}
+ color={'#ce3029'}
+ />
+ {this.state.scanned && this.setState({ scanned: false })} color={'#ce3029'} />}
+
+
+
+
+
+
+ {i18n.t('global.name')}
+ {
+ this.setState({ parsedName: itemValue });
+ }}>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.firstname')}
+
+ this.setState({ parsedFirstname: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.email')}
+
+ this.setState({ parsedEmail: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.company')}
+
+ this.setState({ parsedCompany: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.address')}
+
+ this.setState({ parsedAddress: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.phone')}
+
+ this.setState({ parsedPhone: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ this.setState({ showDynContact: false, scanned: false, parsedName: '', parsedFirstname: '', parsedEmail: '', parsedCompany: '', parsedAddress: '', parsedPhone: '' })}
+ color={'#ce3029'}
+ />
+ this.setState({
+ tmpName: this.state.parsedName,
+ tmpFirstname: this.state.parsedFirstname,
+ tmpEmail: this.state.parsedEmail,
+ tmpCompany: this.state.parsedCompany,
+ tmpAddress: this.state.parsedAddress,
+ tmpPhone: this.state.parsedPhone,
+ parsedName: '',
+ parsedFirstname: '',
+ parsedEmail: '',
+ parsedCompany: '',
+ parsedAddress: '',
+ parsedPhone: '',
+ showDynContact: false,
+ scanned: false,
+ })}
+ color={'#ce3029'}
+ />
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ input: {
+ backgroundColor: 'white',
+ margin: 10,
+ padding: 10,
+ },
+ clearCheckbox: {
+ backgroundColor: '#00000000',
+ borderWidth: 0,
+ },
+ clearRadio: {
+ flex: 1,
+ backgroundColor: '#00000000',
+ borderWidth: 0,
+ },
+ select: {
+ flex: 2,
+ margin: 10,
+ backgroundColor: 'white',
+ },
+ textArea: {
+ flex: 1,
+ margin: 10,
+ padding: 10,
+ backgroundColor: 'white',
+ textAlignVertical: 'top',
+ },
+ button: {
+ width: 35,
+ height: 35,
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: 2,
+ },
+});
+
+export default connect(mapStateToProps)(ContactDetailsScreen);
diff --git a/ComApp/src/Views/ContactDetails_RHD.js b/ComApp/src/Views/ContactDetails_RHD.js
new file mode 100644
index 0000000..82c3df9
--- /dev/null
+++ b/ComApp/src/Views/ContactDetails_RHD.js
@@ -0,0 +1,892 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ Button,
+ TextInput,
+ Picker,
+ TouchableOpacity,
+ Text,
+ Alert,
+ ScrollView
+} from 'react-native';
+import { CheckBox, ButtonGroup, Overlay } from '@rneui/themed';
+import { connect } from 'react-redux';
+import Icon from 'react-native-vector-icons/FontAwesome';
+import { showMessage } from 'react-native-flash-message';
+import Helpers from "../Controllers/Helpers";
+import i18n from "../I18n";
+import { BarCodeScanner } from 'expo-barcode-scanner';
+import vCard from 'vcard-parser';
+
+function mapStateToProps(state) {
+ return {
+ contacts: state.contacts,
+ currentUser: state.currentUser,
+ };
+}
+
+let date = new Date().getDate();
+if (date < 10) {
+ date = '0' + date.toString();
+}
+let month = new Date().getMonth() + 1;
+if (month < 10) {
+ month = '0' + month.toString();
+}
+const year = new Date().getFullYear();
+
+class ContactDetailsScreen extends React.Component {
+ state = {
+ action: 'add',
+ visits: [],
+ selectedVisit: 0,
+ tmpVisitNote: '',
+ newVisitTitle: date + '/' + month + '/' + year,
+ selectedIndex: 0,
+ showNewVisit: false,
+ tmpID: 0,
+ tmpNote: '',
+ tmpName: '',
+ tmpFirstname: '',
+ tmpEmail: '',
+ tmpCompany: '',
+ tmpAddress: '',
+ tmpPhone: '',
+ tmpSelect1: '',
+ tmpSelect2: '',
+ tmpOther: '',
+ clientChecked: false,
+ prospectChecked: false,
+ otherChecked: false,
+ p1: false,
+ p2: false,
+ p3: false,
+ p4: false,
+ p5: false,
+ p6: false,
+ p7: false,
+ p8: false,
+ p9: false,
+ showQRcodevCard: false,
+ parsedType: null,
+ parsedDatas: [],
+ hasPermission: null,
+ scanned: false,
+ showDynContact: false,
+ parsedName: '',
+ parsedFirstname: '',
+ parsedEmail: '',
+ parsedCompany: '',
+ parsedAddress: '',
+ parsedPhone: ''
+ };
+
+ updateContact() {
+ let error = '';
+ const { action } = this.props.route.params;
+ // On récupère le texte courant
+ if (this.state.visits.length > 0) {
+ this.state.visits[
+ this.state.selectedVisit
+ ].note = this.state.tmpVisitNote;
+ }
+ let type;
+ if (this.state.clientChecked === true) {
+ type = 'client';
+ } else if (this.state.prospectChecked === true) {
+ type = 'prospect';
+ } else if (this.state.otherChecked === true) {
+ type = 'other';
+ if (this.state.tmpOther.length === 0) {
+ error = error.concat(i18n.t('error.fill_other_field') + '.\n');
+ }
+ } else {
+ error = error.concat(i18n.t('error.specify_type_contact') + '.\n');
+ }
+ if (this.state.tmpEmail.length < 5 || !Helpers.isEmailValid(this.state.tmpEmail)) {
+ error = error.concat(i18n.t('error.invalid_email_address') + '.\n');
+ }
+ if (this.state.tmpName.length === 0) {
+ error = error.concat(i18n.t('error.invalid_name') + '.\n');
+ }
+ if (this.state.tmpFirstname.length === 0) {
+ error = error.concat(i18n.t('error.invalid_firstname') + '.\n');
+ }
+
+ if (error.length > 0) {
+ Alert.alert(i18n.t('global.warning') + ' !', error, [
+ { text: 'OK' },
+ ],
+ { cancelable: false },
+ );
+ } else {
+ const products = [
+ this.state.p1,
+ this.state.p2,
+ this.state.p3,
+ this.state.p4,
+ this.state.p5,
+ this.state.p6,
+ this.state.p7,
+ this.state.p8,
+ this.state.p9
+ ];
+ const contact = {
+ id: Helpers.generateUUID(),
+ email: this.state.tmpEmail,
+ type: type,
+ other: this.state.tmpOther,
+ select1: this.state.tmpSelect1,
+ select2: this.state.tmpSelect2,
+ name: this.state.tmpName,
+ firstname: this.state.tmpFirstname,
+ company: this.state.tmpCompany,
+ address: this.state.tmpAddress,
+ phone: this.state.tmpPhone,
+ products: JSON.stringify(products),
+ notes: this.state.tmpNote,
+ visits: JSON.stringify(this.state.visits),
+ to_delete: false,
+ synced: false,
+ };
+ if (action === 'edit') {
+ contact.id = this.state.tmpID;
+ this.props.dispatch({ type: 'UPDATE_CONTACT', data: contact });
+ this.populate();
+ showMessage({
+ message: i18n.t('contact_details_s.contact_saved') + '.',
+ type: 'success',
+ duration: 500,
+ });
+ } else {
+ contact.user_id = this.props.currentUser.id;
+ this.props.dispatch({ type: 'ADD_CONTACT', data: contact });
+ this.props.navigation.navigate('ContactScreen', { contactAdded: true });
+ }
+ }
+ }
+
+ updateContactType(type) {
+ if (type === 'client') {
+ this.setState({
+ clientChecked: true,
+ prospectChecked: false,
+ otherChecked: false,
+ tmpOther: '',
+ });
+ } else if (type === 'prospect') {
+ this.setState({
+ clientChecked: false,
+ prospectChecked: true,
+ otherChecked: false,
+ tmpOther: '',
+ });
+ } else {
+ this.setState({
+ clientChecked: false,
+ prospectChecked: false,
+ otherChecked: true,
+ });
+ }
+ }
+
+ newVisit() {
+ this.state.visits.unshift({ title: this.state.newVisitTitle, note: '' });
+ this.state.visits[this.state.selectedVisit].note = this.state.tmpVisitNote;
+ this.setState({
+ tmpVisitNote: '',
+ visits: this.state.visits,
+ showNewVisit: false,
+ selectedVisit: 0,
+ });
+ }
+
+ deleteVisit() {
+ Alert.alert(
+ i18n.t('global.confirmation'),
+ i18n.t('contact_details_s.confirm_visit_delete'),
+ [
+ {
+ text: i18n.t('global.no'),
+ onPress: () => console.log('Cancel Pressed'),
+ style: 'cancel',
+ },
+ {
+ text: i18n.t('global.yes'),
+ onPress: () => {
+ this.state.visits.splice(this.state.selectedVisit, 1);
+ if (this.state.visits.length === 0) {
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: 0,
+ tmpVisitNote: '',
+ });
+ } else {
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: 0,
+ tmpVisitNote: this.state.visits[0].note,
+ });
+ }
+ },
+ },
+ ],
+ { cancelable: false },
+ );
+ }
+
+ populate() {
+ const { contactId } = this.props.route.params;
+ const contact = this.props.contacts.filter(tmp => tmp.id === contactId)[0];
+ this.setState({
+ tmpID: contact.id,
+ tmpNote: contact.notes,
+ tmpName: contact.name,
+ tmpFirstname: contact.firstname,
+ tmpEmail: contact.email,
+ tmpCompany: contact.company,
+ tmpAddress: contact.address,
+ tmpPhone: contact.phone,
+ tmpSelect1: contact.select1,
+ tmpSelect2: contact.select2,
+ tmpOther: contact.other,
+ tmpProducts: contact.product,
+ });
+ if (contact.type === 'client') {
+ this.setState({ clientChecked: true });
+ } else if (contact.type === 'prospect') {
+ this.setState({ prospectChecked: true });
+ } else {
+ this.setState({ otherChecked: true });
+ }
+ if (contact.visits !== null) {
+ const tmp = JSON.parse(contact.visits);
+ this.setState({ visits: tmp });
+ if (tmp.length > 0) {
+ this.setState({ tmpVisitNote: tmp[this.state.selectedVisit].note });
+ }
+ }
+ if (contact.products !== null) {
+ const tmp = JSON.parse(contact.products);
+ this.setState({
+ p1: tmp[0],
+ p2: tmp[1],
+ p3: tmp[2],
+ p4: tmp[3],
+ p5: tmp[4],
+ p6: tmp[5],
+ p7: tmp[6],
+ p8: tmp[7],
+ p9: tmp[8]
+ });
+ }
+ }
+
+ componentDidMount() {
+ const { action } = this.props.route.params;
+ if (action === 'edit') {
+ this.setState({ action: 'edit' });
+ this.populate();
+ }
+
+ BarCodeScanner.requestPermissionsAsync().then(res => {
+ this.setState({ hasPermission: res.status === 'granted' });
+ });
+
+ }
+
+ render() {
+ const handleBarCodeScanned = ({ type, data }) => {
+ this.setState({ scanned: true });
+ if (data.includes('VCARD')) {
+ let datas = vCard.parse(data);
+ let tmpDatas = [];
+ if (datas['n'] !== undefined) {
+ const nx = Object.assign({ n1: [{ value: datas['n'][0].value[0] }] }, { n2: [{ value: datas['n'][0].value[1] }] });
+ datas = Object.assign(datas, nx);
+ }
+ if (datas['adr'] !== undefined) {
+ let tmp = datas['adr'][0].value;
+ let tmpadr = '';
+ for (let i = 0; i < tmp.length; i++) {
+ if (tmp[i].length > 0) {
+ if (tmpadr.length > 0) {
+ tmpadr = tmpadr + ', ' + tmp[i];
+ } else {
+ tmpadr = tmp[i];
+ }
+ }
+ }
+ datas['adr'] = [{ value: tmpadr }];
+ }
+ Object.keys(datas).map((objectKey, index) => {
+ if (objectKey === 'email' || objectKey === 'adr' || objectKey === 'tel' || objectKey === 'org' || objectKey === 'n1' || objectKey === 'n2') {
+ let value = datas[objectKey][0].value;
+ if (typeof value === 'string') {
+ tmpDatas.push(value);
+ }
+ }
+ });
+ this.setState({ parsedType: 'vcard', parsedDatas: tmpDatas, showQRcodevCard: false, showDynContact: true })
+ } else {
+ Alert.alert(i18n.t('contact_details_s.qrcode_not_vcard_format'));
+ }
+ };
+ return (
+
+
+
+ this.updateContactType('client')}
+ />
+ this.updateContactType('prospect')}
+ />
+ this.updateContactType('other')}
+ />
+ {this.state.otherChecked === true && (
+ this.setState({ tmpOther: text })}
+ />
+ )}
+
+
+ this.setState({ tmpName: text })}
+ />
+ this.setState({ tmpFirstname: text })}
+ />
+
+ this.setState({ tmpEmail: text })}
+ autoCapitalize={'none'}
+ autoCorrect={false}
+ keyboardType={'email-address'}
+ />
+ this.setState({ tmpCompany: text })}
+ />
+ this.setState({ tmpAddress: text })}
+ />
+ this.setState({ tmpPhone: text })}
+ autoCompleteType={'tel'}
+ dataDetectorTypes={'phoneNumber'}
+ keyboardType={'phone-pad'}
+ />
+
+
+
+ {
+ if (itemValue !== '0') {
+ this.setState({ tmpSelect1: itemValue });
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (itemValue !== '0') {
+ this.setState({ tmpSelect2: itemValue });
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+ this.setState({ p1: !this.state.p1 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p2: !this.state.p2 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p3: !this.state.p3 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p4: !this.state.p4 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p5: !this.state.p5 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p6: !this.state.p6 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p7: !this.state.p7 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p8: !this.state.p8 })}
+ containerStyle={styles.clearCheckbox}
+ />
+ this.setState({ p9: !this.state.p9 })}
+ containerStyle={styles.clearCheckbox}
+ />
+
+
+
+
+ this.setState({ selectedIndex: index })}
+ />
+ {this.state.selectedIndex === 0 && (
+
+
+ {this.state.visits.length > 0 && (
+
+
+ {
+ this.state.visits[
+ this.state.selectedVisit
+ ].note = this.state.tmpVisitNote;
+ this.setState({
+ visits: this.state.visits,
+ selectedVisit: itemIndex,
+ tmpVisitNote: this.state.visits[itemIndex].note,
+ });
+ }}>
+ {this.state.visits.map((item, index) => {
+ return ;
+ })}
+
+
+ )}
+ {this.state.visits.length === 0 && (
+
+ {i18n.t('contact_details_s.no_visits')}.
+
+ )}
+
+ this.setState({ showNewVisit: true })}>
+
+
+ {this.state.visits.length > 0 && (
+ this.deleteVisit()}>
+
+
+ )}
+
+
+ {this.state.visits.length > 0 && (
+ this.setState({ tmpVisitNote: text })}
+ />
+ )}
+
+ )}
+ {this.state.selectedIndex === 1 && (
+ this.setState({ tmpNote: text })}
+ />
+ )}
+
+
+ {this.state.action === 'add' && (
+ <>
+ this.setState({ showQRcodevCard: true })} color={'#ce3029'} />
+ >
+ )}
+ this.updateContact()} color={'#ce3029'} />
+
+
+
+
+ {i18n.t('contact_details_s.visit_name')} :
+ this.setState({ newVisitTitle: text })}
+ style={{
+ borderWidth: 1,
+ borderColor: 'lightgrey',
+ width: 300,
+ marginBottom: 15,
+ paddingLeft: 10,
+ }}
+ />
+
+ this.setState({ showNewVisit: false })}
+ color={'#ce3029'}
+ />
+ this.newVisit()} color={'#ce3029'} />
+
+
+
+
+
+ {this.state.hasPermission === true && (
+
+ )}
+ {this.state.hasPermission === false && (
+ {i18n.t('error.camera_permission_denied')}.
+ )}
+ {this.state.hasPermission === null && (
+ {i18n.t('error.pending_camera_permission')}.
+ )}
+
+ this.setState({ showQRcodevCard: false, scanned: false })}
+ color={'#ce3029'}
+ />
+ {this.state.scanned && this.setState({ scanned: false })} color={'#ce3029'} />}
+
+
+
+
+
+
+ {i18n.t('global.name')}
+ {
+ this.setState({ parsedName: itemValue });
+ }}>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.firstname')}
+
+ this.setState({ parsedFirstname: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.email')}
+
+ this.setState({ parsedEmail: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.company')}
+
+ this.setState({ parsedCompany: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.address')}
+
+ this.setState({ parsedAddress: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ {i18n.t('global.phone')}
+
+ this.setState({ parsedPhone: itemValue })
+ }>
+
+ {this.state.parsedType === 'vcard' && this.state.parsedDatas.map((item, index) => {
+ return ;
+ })}
+
+
+
+ this.setState({ showDynContact: false, scanned: false, parsedName: '', parsedFirstname: '', parsedEmail: '', parsedCompany: '', parsedAddress: '', parsedPhone: '' })}
+ color={'#ce3029'}
+ />
+ this.setState({
+ tmpName: this.state.parsedName,
+ tmpFirstname: this.state.parsedFirstname,
+ tmpEmail: this.state.parsedEmail,
+ tmpCompany: this.state.parsedCompany,
+ tmpAddress: this.state.parsedAddress,
+ tmpPhone: this.state.parsedPhone,
+ parsedName: '',
+ parsedFirstname: '',
+ parsedEmail: '',
+ parsedCompany: '',
+ parsedAddress: '',
+ parsedPhone: '',
+ showDynContact: false,
+ scanned: false,
+ })}
+ color={'#ce3029'}
+ />
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ input: {
+ backgroundColor: 'white',
+ margin: 10,
+ padding: 10,
+ },
+ clearCheckbox: {
+ backgroundColor: '#00000000',
+ borderWidth: 0,
+ },
+ clearRadio: {
+ flex: 1,
+ backgroundColor: '#00000000',
+ borderWidth: 0,
+ },
+ select: {
+ flex: 2,
+ margin: 10,
+ backgroundColor: 'white',
+ },
+ textArea: {
+ flex: 1,
+ margin: 10,
+ padding: 10,
+ backgroundColor: 'white',
+ textAlignVertical: 'top',
+ },
+ button: {
+ width: 35,
+ height: 35,
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: 2,
+ },
+});
+
+export default connect(mapStateToProps)(ContactDetailsScreen);
diff --git a/ComApp/src/Views/Directories.js b/ComApp/src/Views/Directories.js
new file mode 100644
index 0000000..86f1016
--- /dev/null
+++ b/ComApp/src/Views/Directories.js
@@ -0,0 +1,114 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Text,
+} from 'react-native';
+import {connect} from 'react-redux';
+import TreeView from 'react-native-animated-tree-view';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+
+function mapStateToProps(state) {
+ return {
+ documents: state.documents,
+ };
+}
+
+class DirectoriesScreen extends React.Component {
+ state = {
+ documents: [],
+ };
+
+ showDocuments(folder) {
+ const documents = this.props.documents.filter(
+ tmp => tmp.directory === folder.id,
+ );
+ this.setState({documents: documents});
+ }
+
+ openDocument(document) {
+ this.props.dispatch({type: 'SET_CURRENT_OPENED_DOC', data: document});
+ if (document.type === 'pdf') {
+ this.props.navigation.navigate('PDFScreen', {uuid: document.uuid});
+ } else if (document.type === 'mp4') {
+ this.props.navigation.navigate('VideoScreen', {uuid: document.uuid});
+ } else {
+ console.log('Inconnu');
+ }
+ }
+
+ render() {
+ const {folder} = this.props.route.params;
+ let documentList = this.state.documents.map(document => {
+ return (
+ this.openDocument(document)}>
+ {document.type === 'pdf' ? (
+
+ ) : (
+
+ )}
+ {document.title}
+
+ );
+ });
+
+ return (
+
+
+ this.showDocuments(tmp)}
+ />
+
+
+ {documentList}
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ treeviewContainer: {
+ width: 290,
+ },
+ scrollview: {
+ flex: 2,
+ },
+ filesContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ file: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 200,
+ width: 180,
+ backgroundColor: '#ce3029',
+ margin: 10,
+ },
+ fileTxt: {
+ textAlign: 'center',
+ color: 'white',
+ fontWeight: 'bold',
+ fontSize: 18,
+ padding: 5
+ }
+});
+
+export default connect(mapStateToProps)(DirectoriesScreen);
diff --git a/ComApp/src/Views/Home.js b/ComApp/src/Views/Home.js
new file mode 100644
index 0000000..d5ae241
--- /dev/null
+++ b/ComApp/src/Views/Home.js
@@ -0,0 +1,116 @@
+import React from 'react';
+import { View, StyleSheet, ImageBackground, TouchableOpacity, Text, Image } from 'react-native';
+import { connect } from 'react-redux';
+import NetInfo from '@react-native-community/netinfo';
+import Sync from '../Controllers/Sync';
+//import SplashScreen from 'react-native-splash-screen';
+import i18n from "../I18n";
+
+function mapStateToProps(state) {
+ return {
+ directories: state.directories,
+ locale: state.locale,
+ };
+}
+
+class HomeScreen extends React.Component {
+ state = {
+ lastSyncCall: '2020-02-12',
+ };
+ componentDidMount(): void {
+ this.props.dispatch({ type: 'INIT' });
+ //SplashScreen.hide();
+ }
+
+ unsubscribe() {
+ NetInfo.addEventListener(state => {
+ if (state.isConnected === true) {
+ let seconds =
+ Math.abs(new Date(this.state.lastSyncCall) - Date.now()) / 1000;
+ //console.log(seconds);
+ if (seconds > 30) {
+ this.setState({ lastSyncCall: Date.now() });
+ Sync.needSync(this.openSync.bind(this));
+ }
+ }
+ });
+ }
+
+ openSync() {
+ this.props.navigation.navigate('SyncScreen', { isInstalled: true });
+ }
+
+ openFolder(folderID) {
+ const folder = this.props.directories.filter(tmp => tmp.id === folderID);
+ this.props.navigation.navigate('DirectoriesScreen', { folder: folder });
+ }
+
+ render() {
+ i18n.locale = this.props.locale;
+ this.unsubscribe();
+ return (
+
+
+ this.openFolder(2)} style={styles.button}>
+
+ {i18n.t('home_s.1')}
+
+ this.openFolder(3)} style={styles.button}>
+
+ {i18n.t('home_s.2')}
+
+ this.openFolder(4)} style={styles.button}>
+
+ {i18n.t('home_s.3')}
+
+ this.props.navigation.navigate('OnlyVideosScreen')} style={styles.button}>
+
+ {i18n.t('global.videos')}
+
+ this.props.navigation.navigate('ContactScreen')} style={styles.button}>
+
+ {i18n.t('global.contacts')}
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'flex-end',
+ justifyContent: 'center',
+ },
+ text: {
+ height: 40,
+ width: 300,
+ borderColor: 'grey',
+ borderWidth: 1,
+ marginBottom: 20,
+ },
+ button: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'white',
+ marginBottom: 20,
+ marginRight: 50,
+ width: 400,
+ height: 80,
+ borderRadius: 10
+ },
+ btnTxt: {
+ fontSize: 20
+ },
+ btnImg: {
+ position: 'absolute',
+ left: 15,
+ height: 50,
+ width: 50,
+ resizeMode: 'stretch'
+ }
+});
+
+export default connect(mapStateToProps)(HomeScreen);
diff --git a/ComApp/src/Views/Login.js b/ComApp/src/Views/Login.js
new file mode 100644
index 0000000..862665d
--- /dev/null
+++ b/ComApp/src/Views/Login.js
@@ -0,0 +1,163 @@
+import React from 'react';
+import { View, StyleSheet, TextInput, Button, Alert, ImageBackground, Image } from 'react-native';
+import { connect } from 'react-redux';
+import { verify } from 'react-native-b-crypt-bridge';
+import Sync from '../Controllers/Sync';
+import CommonModel from '../Models/Common';
+//import SplashScreen from 'react-native-splash-screen';
+import NetInfo from "@react-native-community/netinfo";
+import i18n from "../I18n";
+
+function mapStateToProps(state) {
+ return {
+ users: state.users,
+ locale: state.locale,
+ };
+}
+
+class LoginScreen extends React.Component {
+ state = {
+ login: '',
+ password: '',
+ };
+
+ componentDidMount(): void {
+ //SplashScreen.hide();
+ }
+
+ login(login, password) {
+ if (login !== '' && password !== '') {
+ let user = this.props.users.filter(tmp => tmp.email === login);
+ if (user.length > 0) {
+ user = user[0];
+ verify(
+ password,
+ user.password
+ ).then(verified => {
+ if (verified === true) {
+ // Si user === last user
+ const lastUser = CommonModel.get('lastUser');
+ if (lastUser !== false && lastUser !== undefined && lastUser.value === user.id.toString()) {
+ this.loginAction(user);
+ } else {
+ // Si la tablette est synchronisée avec le serveur
+ if (Sync.isSynced()) {
+ // Si la tablette est connectée à internet pour forcer la synchro
+ NetInfo.fetch().then(state => {
+ if (state.isConnected) {
+ this.loginAction(user, true);
+ } else {
+ Alert.alert(i18n.t('error.tablet_not_connected_internet'));
+ }
+ });
+ } else {
+ Alert.alert(i18n.t('error.tablet_not_synced_last_user'));
+ }
+ }
+ } else {
+ Alert.alert(i18n.t('error.incorrect_credentials'));
+ }
+ }).catch(error => console.log(error));
+ } else {
+ Alert.alert(i18n.t('error.incorrect_credentials'));
+ }
+ } else {
+ Alert.alert(i18n.t('error.must_fill_login_fields'));
+ }
+ }
+
+ loginAction(user, forceSync = false) {
+ this.setState({ login: '', password: '' });
+ CommonModel.update({
+ key: 'isLogged',
+ value: JSON.stringify(user),
+ });
+ CommonModel.update({
+ key: 'loggedTime',
+ value: Date.now().toString(),
+ });
+ CommonModel.update({ key: 'lastUser', value: user.id.toString() });
+ this.props.dispatch({
+ type: 'SET_CURRENT_USER',
+ data: JSON.stringify(user),
+ });
+ this.props.dispatch({ type: 'SET_LOGGED', data: true });
+ if (forceSync === true) {
+ this.props.navigation.navigate('SyncScreen', { isInstalled: true, forceSync: true });
+ }
+ }
+
+ renderInstallBtn() {
+ if (!Sync.isInstalled()) {
+ return (
+
+
+ this.props.navigation.navigate('SyncScreen', { isInstalled: false })
+ }
+ />
+
+ );
+ }
+ }
+
+ render() {
+ i18n.locale = this.props.locale;
+ return (
+
+
+
+ {Sync.isInstalled() && (
+
+ this.setState({ login: text })}
+ autoCapitalize={'none'}
+ autoCorrect={false}
+ keyboardType={'email-address'}
+ />
+ this.setState({ password: text })}
+ secureTextEntry={true}
+ autoCapitalize={'none'}
+ autoCorrect={false}
+ />
+ this.login(this.state.login, this.state.password)}
+ color={'#ce3029'}
+ />
+
+ )}
+ {this.renderInstallBtn()}
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ text: {
+ height: 40,
+ width: 300,
+ borderColor: 'grey',
+ borderWidth: 1,
+ marginBottom: 20,
+ backgroundColor: 'white',
+ paddingLeft: 10
+ },
+});
+
+export default connect(mapStateToProps)(LoginScreen);
diff --git a/ComApp/src/Views/MySpace.js b/ComApp/src/Views/MySpace.js
new file mode 100644
index 0000000..7c3d607
--- /dev/null
+++ b/ComApp/src/Views/MySpace.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Text,
+} from 'react-native';
+import {connect} from 'react-redux';
+
+function mapStateToProps(state) {
+ return {
+ documents: state.documents,
+ };
+}
+
+class MySpaceScreen extends React.Component {
+
+ render() {
+ return (
+
+ Mon Espace
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+});
+
+export default connect(mapStateToProps)(MySpaceScreen);
diff --git a/ComApp/src/Views/OnlyVideos.js b/ComApp/src/Views/OnlyVideos.js
new file mode 100644
index 0000000..0c7f041
--- /dev/null
+++ b/ComApp/src/Views/OnlyVideos.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Text,
+} from 'react-native';
+import {connect} from 'react-redux';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+
+function mapStateToProps(state) {
+ return {
+ documents: state.documents,
+ };
+}
+
+class OnlyVideosScreen extends React.Component {
+ state = {
+ documents: [],
+ };
+
+ showDocuments() {
+ const documents = this.props.documents.filter(tmp => tmp.type === 'mp4');
+ this.setState({documents: documents});
+ }
+
+ openDocument(document) {
+ this.props.dispatch({type: 'SET_CURRENT_OPENED_DOC', data: document});
+ this.props.navigation.navigate('VideoScreen', {uuid: document.uuid});
+ }
+
+ componentDidMount(): void {
+ this.showDocuments();
+ }
+
+ render() {
+ let documentList = this.state.documents.map(document => {
+ return (
+ this.openDocument(document)}>
+ {document.type === 'pdf' ? (
+
+ ) : (
+
+ )}
+ {document.title}
+
+ );
+ });
+
+ return (
+
+
+ {documentList}
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ scrollview: {
+ flex: 2,
+ },
+ filesContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ file: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 200,
+ width: 180,
+ backgroundColor: '#ce3029',
+ margin: 10,
+ },
+ fileTxt: {
+ textAlign: 'center',
+ color: 'white',
+ fontWeight: 'bold',
+ fontSize: 18,
+ padding: 5
+ }
+});
+
+export default connect(mapStateToProps)(OnlyVideosScreen);
diff --git a/ComApp/src/Views/PDF.js b/ComApp/src/Views/PDF.js
new file mode 100644
index 0000000..a863259
--- /dev/null
+++ b/ComApp/src/Views/PDF.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import { View, StyleSheet, Dimensions } from 'react-native';
+
+import Pdf from 'react-native-pdf';
+import * as FileSystem from 'expo-file-system';
+
+class PDFScreen extends React.Component {
+ render() {
+ const { uuid } = this.props.route.params;
+ const source = {
+ uri: FileSystem.documentDirectory + uuid + '.pdf',
+ cache: false,
+ };
+ return (
+
+ {
+ console.log(`number of pages: ${numberOfPages}`);
+ }}
+ onPageChanged={(page, numberOfPages) => {
+ console.log(`current page: ${page}`);
+ }}
+ onError={error => {
+ console.log(error);
+ }}
+ onPressLink={uri => {
+ console.log(`Link presse: ${uri}`);
+ }}
+ style={styles.pdf}
+ />
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ text: {
+ height: 40,
+ width: 300,
+ borderColor: 'grey',
+ borderWidth: 1,
+ marginBottom: 20,
+ },
+ pdf: {
+ flex: 1,
+ width: Dimensions.get('window').width,
+ height: Dimensions.get('window').height,
+ },
+});
+
+export default PDFScreen;
diff --git a/ComApp/src/Views/Search.js b/ComApp/src/Views/Search.js
new file mode 100644
index 0000000..01dc332
--- /dev/null
+++ b/ComApp/src/Views/Search.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import {
+ View,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Text,
+} from 'react-native';
+import {connect} from 'react-redux';
+import Icon from 'react-native-vector-icons/FontAwesome5';
+import Helpers from "../Controllers/Helpers";
+import i18n from "../I18n";
+
+function mapStateToProps(state) {
+ return {
+ documents: state.documents,
+ };
+}
+
+class SearchScreen extends React.Component {
+ state = {
+ documents: [],
+ };
+
+ openDocument(document) {
+ this.props.dispatch({type: 'SET_CURRENT_OPENED_DOC', data: document});
+ if (document.type === 'pdf') {
+ this.props.navigation.navigate('PDFScreen', {uuid: document.uuid});
+ } else if (document.type === 'mp4') {
+ this.props.navigation.navigate('VideoScreen', {uuid: document.uuid});
+ } else {
+ console.log('Inconnu');
+ }
+ }
+
+ componentDidMount(): void {
+ const {needle, onlyVideos} = this.props.route.params;
+ let objDocs = this.props.documents;
+ let arrayDocs = Object.keys(objDocs).map(function (key) { return objDocs[key]; });
+ let sorted = arrayDocs.sort(Helpers.compareValues('title', 'asc'));
+ let filtered = sorted.filter(item => item.title.toLowerCase().indexOf(needle.toLowerCase()) >= 0);
+ if (onlyVideos === true) {
+ filtered = filtered.filter(item => item.type === 'mp4');
+ }
+ this.setState({
+ documents: filtered,
+ });
+ }
+
+ render() {
+ let documentList = this.state.documents.map(document => {
+ return (
+ this.openDocument(document)}>
+ {document.type === 'pdf' ? (
+
+ ) : (
+
+ )}
+ {document.title}
+
+ );
+ });
+
+ return (
+
+
+ {documentList}
+ {this.state.documents.length === 0 && (
+ {i18n.t('search_s.no_result')}
+ )}
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ scrollview: {
+ flex: 2,
+ },
+ filesContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ file: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 200,
+ width: 180,
+ backgroundColor: '#ce3029',
+ margin: 10,
+ },
+ fileTxt: {
+ textAlign: 'center',
+ color: 'white',
+ fontWeight: 'bold',
+ fontSize: 18,
+ padding: 5
+ }
+});
+
+export default connect(mapStateToProps)(SearchScreen);
diff --git a/ComApp/src/Views/Sync.js b/ComApp/src/Views/Sync.js
new file mode 100644
index 0000000..87ed5d1
--- /dev/null
+++ b/ComApp/src/Views/Sync.js
@@ -0,0 +1,320 @@
+import React from 'react';
+import {View, StyleSheet, Button, Alert, Text, Linking} from 'react-native';
+import {connect} from 'react-redux';
+import * as FileSystem from 'expo-file-system';
+
+import Sync from '../Controllers/Sync';
+
+import * as Progress from 'react-native-progress';
+import NetInfo from '@react-native-community/netinfo';
+
+import i18n from "../I18n";
+
+function mapStateToProps(state) {
+ return {
+ documents: state.documents,
+ syncVersion: state.syncVersion,
+ currentUser: state.currentUser,
+ };
+}
+
+class SyncScreen extends React.Component {
+ state = {
+ progress: 0,
+ txtStep: '',
+ downloadProgress: 0,
+ downloadingFile: false,
+ processing: false,
+ newApp: false
+ };
+
+ callback(downloadProgress) {
+ let progress =
+ (downloadProgress.totalBytesWritten /
+ downloadProgress.totalBytesExpectedToWrite) *
+ 100;
+ progress = Math.round(progress);
+ progress = progress.toString();
+ this.setState({
+ downloadProgress: progress,
+ });
+ }
+
+ downloadResumable(url, uuid, type) {
+ return FileSystem.createDownloadResumable(
+ url,
+ FileSystem.documentDirectory + uuid + '.' + type,
+ {},
+ this.callback.bind(this),
+ );
+ }
+
+ async download(documents, progress, i) {
+ if (this.state.downloadingFile === false) {
+ this.setState({downloadingFile: true});
+ }
+ const endpoint = Sync.getEndpointDocs();
+ const doc = documents[i];
+ this.setState({
+ txtStep:
+ i18n.t('sync_s.download') + ' : ' +
+ doc.title +
+ ' (' +
+ doc.type.toUpperCase() +
+ ') ...',
+ });
+ try {
+ //console.log(endpoint + doc.uuid + "." + doc.type);
+ const {uri} = await this.downloadResumable(
+ endpoint + doc.uuid + "." + doc.type,
+ doc.uuid,
+ doc.type,
+ ).downloadAsync();
+ const {modificationTime} = await FileSystem.getInfoAsync(FileSystem.documentDirectory + doc.uuid + '.' + doc.type);
+ Sync.updateLastModificationDoc(doc.id, modificationTime);
+ //console.log('Finished downloading to ', uri);
+ this.setState({progress: this.state.progress + progress});
+ if (i < documents.length - 1) {
+ return this.download(documents, progress, i + 1);
+ }
+ this.setState({downloadingFile: false});
+ return true;
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+ }
+
+ async deleteFile(docs, i) {
+ if (docs.length > 0) {
+ const file = docs[i];
+ await FileSystem.deleteAsync(FileSystem.documentDirectory + file.uuid + '.' + file.type, {idempotent: true});
+ if (i < docs.length - 1) {
+ return this.deleteFile(docs, i + 1);
+ }
+ }
+ return true;
+ }
+
+ async getToDL(docs, i, toDL = []) {
+ if (docs.length < 1)
+ return toDL;
+ const item = docs[i];
+ const {exists, modificationTime} = await FileSystem.getInfoAsync(FileSystem.documentDirectory + item.uuid + '.' + item.type);
+ if (exists === false) {
+ toDL.push(item);
+ } else {
+ if (modificationTime.toString() !== item.updated_at) {
+ toDL.push(item);
+ }
+ }
+ if (i < docs.length - 1) {
+ return this.getToDL(docs, i + 1, toDL);
+ }
+ return toDL;
+ }
+
+ downloadDocuments(docs, toDelete) {
+ const version = this.props.syncVersion;
+ this.getToDL(docs, 0).then(toDownload => {
+ if (toDownload.length > 0) {
+ this.setState({progress: 0.5});
+ const docProgress = 0.5 / toDownload.length;
+ this.download(toDownload, docProgress, 0).then(status => {
+ if (status === true) {
+ this.deleteFile(toDelete, 0).then(status => {
+ this.finishSync(version);
+ });
+ } else {
+ this.setState({txtStep: i18n.t('error.error_has_occurred') + '.'});
+ }
+ });
+ } else {
+ this.deleteFile(toDelete, 0).then(status => {
+ this.setState({progress: 1});
+ this.finishSync(version);
+ });
+ }
+ });
+ }
+
+ finishSync(version) {
+ this.props.dispatch({type: 'INIT'});
+ Sync.updateVersion(version);
+ this.setState({txtStep: i18n.t('sync_s.update_completed') + '.', processing: false});
+ if (this.state.newApp === true) {
+ const appUUID = Sync.getAppUUID();
+ const url = 'https://store.ldc.fr' + '/app/' + appUUID;
+ Alert.alert(
+ 'LDC Store',
+ i18n.t('sync_s.new_app_version'),
+ [
+ {
+ text: i18n.t('global.cancel'),
+ onPress: () => console.log('Cancel Pressed'),
+ style: 'cancel',
+ },
+ {text: i18n.t('sync_s.open_store'), onPress: () => Linking.openURL(url).catch((err) => console.error('An error occurred', err))},
+ ],
+ {cancelable: false},
+ );
+ }
+ }
+
+ getDocuments() {
+ this.setState({progress: 0.4, txtStep: i18n.t('sync_s.updating_docs') + '...'});
+ Sync.getDocuments(this.downloadDocuments.bind(this));
+ }
+
+ getDirectories() {
+ this.setState({progress: 0.3, txtStep: i18n.t('sync_s.updating_folders') + '...'});
+ Sync.getDirectories(this.getDocuments.bind(this));
+ }
+
+ sendBrochures() {
+ this.setState({progress: 0.2, txtStep: i18n.t('sync_s.sending_flyers') + '...'});
+ Sync.sendBrochures(this.getDirectories.bind(this));
+ }
+
+ getContacts() {
+ this.setState({progress: 0.1, txtStep: i18n.t('sync_s.updating_contacts') + '...'});
+ Sync.getContacts(this.sendBrochures.bind(this), this.props.currentUser.id);
+ }
+
+ installProcessed() {
+ Sync.setInstalled();
+ this.props.dispatch({type: 'INIT_USERS'});
+ this.setState({
+ processing: false,
+ progress: 1,
+ txtStep: i18n.t('sync_s.user_install_completed') + '.',
+ });
+ }
+
+ process(version, ShowIsActive, newApp) {
+ this.props.dispatch({
+ type: 'UPDATE_VERSION',
+ data: version,
+ });
+ this.props.dispatch({
+ type: 'UPDATE_SHOW_IS_ACTIVE',
+ data: ShowIsActive,
+ });
+ this.setState({newApp: newApp});
+ const {isInstalled} = this.props.route.params;
+ if (isInstalled === false) {
+ this.setState({txtStep: i18n.t('sync_s.user_install') + '...'});
+ Sync.getUsers(this.installProcessed.bind(this));
+ } else {
+ this.setState({txtStep: i18n.t('sync_s.updating_users') + '...'});
+ Sync.getUsers(this.getContacts.bind(this));
+ }
+ }
+
+ fileProgress() {
+ if (this.state.downloadingFile === true) {
+ return this.state.downloadProgress + ' %';
+ }
+ }
+
+ errorCallback(error) {
+ let msg = '';
+ let httpCode = '';
+ if (error.response) {
+ // The request was made and the server responded with a status code
+ // that falls out of the range of 2xx
+ console.log(error.response.data);
+ console.log(error.response.status);
+ console.log(error.response.headers);
+ msg = i18n.t('error.error_has_occurred') + '. Err: 1';
+ httpCode = error.response.status;
+ } else if (error.request) {
+ // The request was made but no response was received
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
+ // http.ClientRequest in node.js
+ //console.log(error.request);
+ msg = i18n.t('error.error_has_occurred') + '. Err: 2';
+ } else {
+ // Something happened in setting up the request that triggered an Error
+ //console.log('Error', error.message);
+ msg = i18n.t('error.error_has_occurred') + '. Err: 3';
+ }
+ if (httpCode.length > 0) {
+ Alert.alert(
+ i18n.t('error.error'),
+ msg + ' (Code: ' + httpCode + ')',
+ [{text: 'OK'}],
+ {cancelable: false},
+ );
+ }else{
+ Alert.alert(
+ i18n.t('error.error'),
+ msg,
+ [{text: 'OK'}],
+ {cancelable: false},
+ );
+ }
+ this.setState({processing: false});
+ }
+
+ checkConnection() {
+ NetInfo.fetch().then(state => {
+ if (state.isConnected) {
+ this.setState({processing: true});
+ Sync.getVersion(this.process.bind(this), this.errorCallback.bind(this));
+ } else {
+ Alert.alert(
+ i18n.t('global.warning'),
+ i18n.t('error.must_be_connected_internet_sync') + '.',
+ [{text: 'OK'}],
+ {cancelable: false},
+ );
+ }
+ });
+ return false;
+ }
+
+ componentDidMount() {
+ const {forceSync} = this.props.route.params;
+ if (forceSync === true) {
+ this.checkConnection();
+ }
+ }
+
+ render() {
+ const fileProgress = this.fileProgress();
+ return (
+
+
+
+ this.checkConnection()}
+ />
+
+
+
+ {this.state.txtStep} {fileProgress}
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default connect(mapStateToProps)(SyncScreen);
diff --git a/ComApp/src/Views/Video.js b/ComApp/src/Views/Video.js
new file mode 100644
index 0000000..576f48a
--- /dev/null
+++ b/ComApp/src/Views/Video.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { View, Dimensions } from 'react-native';
+
+import * as FileSystem from 'expo-file-system';
+import VideoPlayer from 'expo-video-player';
+import { ResizeMode } from 'expo-av';
+
+class VideoScreen extends React.Component {
+
+ state = {
+ height: Dimensions.get("window").height - 100
+ }
+
+ render() {
+ console.log("render")
+ const { uuid } = this.props.route.params;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default VideoScreen;
diff --git a/ComApp/src/assets/img/arrow_down.png b/ComApp/src/assets/img/arrow_down.png
new file mode 100644
index 0000000..10eb2a9
Binary files /dev/null and b/ComApp/src/assets/img/arrow_down.png differ
diff --git a/ComApp/src/assets/img/background.jpg b/ComApp/src/assets/img/background.jpg
new file mode 100644
index 0000000..73cd2d6
Binary files /dev/null and b/ComApp/src/assets/img/background.jpg differ
diff --git a/ComApp/src/assets/img/background_intl.jpg b/ComApp/src/assets/img/background_intl.jpg
new file mode 100644
index 0000000..092d2a2
Binary files /dev/null and b/ComApp/src/assets/img/background_intl.jpg differ
diff --git a/ComApp/src/assets/img/background_rhd.jpg b/ComApp/src/assets/img/background_rhd.jpg
new file mode 100644
index 0000000..3778d3e
Binary files /dev/null and b/ComApp/src/assets/img/background_rhd.jpg differ
diff --git a/ComApp/src/assets/img/background_sbv.jpg b/ComApp/src/assets/img/background_sbv.jpg
new file mode 100644
index 0000000..5886473
Binary files /dev/null and b/ComApp/src/assets/img/background_sbv.jpg differ
diff --git a/ComApp/src/assets/img/flags/de.png b/ComApp/src/assets/img/flags/de.png
new file mode 100644
index 0000000..2644cdb
Binary files /dev/null and b/ComApp/src/assets/img/flags/de.png differ
diff --git a/ComApp/src/assets/img/flags/en.png b/ComApp/src/assets/img/flags/en.png
new file mode 100644
index 0000000..49eea3c
Binary files /dev/null and b/ComApp/src/assets/img/flags/en.png differ
diff --git a/ComApp/src/assets/img/flags/es.png b/ComApp/src/assets/img/flags/es.png
new file mode 100644
index 0000000..6a9319d
Binary files /dev/null and b/ComApp/src/assets/img/flags/es.png differ
diff --git a/ComApp/src/assets/img/flags/fr.png b/ComApp/src/assets/img/flags/fr.png
new file mode 100644
index 0000000..d48273c
Binary files /dev/null and b/ComApp/src/assets/img/flags/fr.png differ
diff --git a/ComApp/src/assets/img/guillemets - SBV.png b/ComApp/src/assets/img/guillemets - SBV.png
new file mode 100644
index 0000000..1f8dd64
Binary files /dev/null and b/ComApp/src/assets/img/guillemets - SBV.png differ
diff --git a/ComApp/src/assets/img/guillemets.png b/ComApp/src/assets/img/guillemets.png
new file mode 100644
index 0000000..a109245
Binary files /dev/null and b/ComApp/src/assets/img/guillemets.png differ
diff --git a/ComApp/src/assets/img/guillemets_intl.png b/ComApp/src/assets/img/guillemets_intl.png
new file mode 100644
index 0000000..43502ec
Binary files /dev/null and b/ComApp/src/assets/img/guillemets_intl.png differ
diff --git a/ComApp/src/assets/img/guillemets_rhd.png b/ComApp/src/assets/img/guillemets_rhd.png
new file mode 100644
index 0000000..1250ad9
Binary files /dev/null and b/ComApp/src/assets/img/guillemets_rhd.png differ
diff --git a/ComApp/src/assets/img/icons/android-chrome-192x192.png b/ComApp/src/assets/img/icons/android-chrome-192x192.png
new file mode 100644
index 0000000..ea18b96
Binary files /dev/null and b/ComApp/src/assets/img/icons/android-chrome-192x192.png differ
diff --git a/ComApp/src/assets/img/icons/android-chrome-256x256.png b/ComApp/src/assets/img/icons/android-chrome-256x256.png
new file mode 100644
index 0000000..8e560b2
Binary files /dev/null and b/ComApp/src/assets/img/icons/android-chrome-256x256.png differ
diff --git a/ComApp/src/assets/img/icons/android-chrome-512x512.png b/ComApp/src/assets/img/icons/android-chrome-512x512.png
new file mode 100644
index 0000000..45bd85a
Binary files /dev/null and b/ComApp/src/assets/img/icons/android-chrome-512x512.png differ
diff --git a/ComApp/src/assets/img/icons/apple-touch-icon.png b/ComApp/src/assets/img/icons/apple-touch-icon.png
new file mode 100644
index 0000000..747cc2a
Binary files /dev/null and b/ComApp/src/assets/img/icons/apple-touch-icon.png differ
diff --git a/ComApp/src/assets/img/icons/favicon-16x16.png b/ComApp/src/assets/img/icons/favicon-16x16.png
new file mode 100644
index 0000000..b2e1e7c
Binary files /dev/null and b/ComApp/src/assets/img/icons/favicon-16x16.png differ
diff --git a/ComApp/src/assets/img/icons/favicon-32x32.png b/ComApp/src/assets/img/icons/favicon-32x32.png
new file mode 100644
index 0000000..6c5841d
Binary files /dev/null and b/ComApp/src/assets/img/icons/favicon-32x32.png differ
diff --git a/ComApp/src/assets/img/icons/favicon.ico b/ComApp/src/assets/img/icons/favicon.ico
new file mode 100644
index 0000000..8dc786a
Binary files /dev/null and b/ComApp/src/assets/img/icons/favicon.ico differ
diff --git a/ComApp/src/assets/img/icons/mstile-150x150.png b/ComApp/src/assets/img/icons/mstile-150x150.png
new file mode 100644
index 0000000..10b2c78
Binary files /dev/null and b/ComApp/src/assets/img/icons/mstile-150x150.png differ
diff --git a/ComApp/src/assets/img/icons/safari-pinned-tab.svg b/ComApp/src/assets/img/icons/safari-pinned-tab.svg
new file mode 100644
index 0000000..65c57b4
--- /dev/null
+++ b/ComApp/src/assets/img/icons/safari-pinned-tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ComApp/src/assets/img/left.png b/ComApp/src/assets/img/left.png
new file mode 100644
index 0000000..67f122c
Binary files /dev/null and b/ComApp/src/assets/img/left.png differ
diff --git a/ComApp/src/assets/img/logo - sbv.png b/ComApp/src/assets/img/logo - sbv.png
new file mode 100644
index 0000000..d906b18
Binary files /dev/null and b/ComApp/src/assets/img/logo - sbv.png differ
diff --git a/ComApp/src/assets/img/logo-square - sbv.png b/ComApp/src/assets/img/logo-square - sbv.png
new file mode 100644
index 0000000..7f7f7a4
Binary files /dev/null and b/ComApp/src/assets/img/logo-square - sbv.png differ
diff --git a/ComApp/src/assets/img/logo-square.png b/ComApp/src/assets/img/logo-square.png
new file mode 100644
index 0000000..8fec885
Binary files /dev/null and b/ComApp/src/assets/img/logo-square.png differ
diff --git a/ComApp/src/assets/img/logo-square_intl.png b/ComApp/src/assets/img/logo-square_intl.png
new file mode 100644
index 0000000..c6fb561
Binary files /dev/null and b/ComApp/src/assets/img/logo-square_intl.png differ
diff --git a/ComApp/src/assets/img/logo-square_rhd.png b/ComApp/src/assets/img/logo-square_rhd.png
new file mode 100644
index 0000000..0e9a851
Binary files /dev/null and b/ComApp/src/assets/img/logo-square_rhd.png differ
diff --git a/ComApp/src/assets/img/logo.png b/ComApp/src/assets/img/logo.png
new file mode 100644
index 0000000..2f5252e
Binary files /dev/null and b/ComApp/src/assets/img/logo.png differ
diff --git a/ComApp/src/assets/img/logo_intl.png b/ComApp/src/assets/img/logo_intl.png
new file mode 100644
index 0000000..9cc2f36
Binary files /dev/null and b/ComApp/src/assets/img/logo_intl.png differ
diff --git a/ComApp/src/assets/img/logo_rhd.png b/ComApp/src/assets/img/logo_rhd.png
new file mode 100644
index 0000000..942b805
Binary files /dev/null and b/ComApp/src/assets/img/logo_rhd.png differ
diff --git a/ComApp/src/assets/img/right.png b/ComApp/src/assets/img/right.png
new file mode 100644
index 0000000..6d1ab36
Binary files /dev/null and b/ComApp/src/assets/img/right.png differ
diff --git a/ComApp/yarn.lock b/ComApp/yarn.lock
index b5c6c22..3b091ae 100644
--- a/ComApp/yarn.lock
+++ b/ComApp/yarn.lock
@@ -1098,6 +1098,13 @@
"@colors/colors@1.5.0":
"version" "1.5.0"
+"@egjs/hammerjs@^2.0.17":
+ "integrity" "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="
+ "resolved" "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz"
+ "version" "2.0.17"
+ dependencies:
+ "@types/hammerjs" "^2.0.36"
+
"@eslint/eslintrc@^0.4.3":
"integrity" "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw=="
"resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz"
@@ -2255,6 +2262,11 @@
dependencies:
"@types/node" "*"
+"@types/hammerjs@^2.0.36":
+ "integrity" "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
+ "resolved" "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz"
+ "version" "2.0.41"
+
"@types/hoist-non-react-statics@^3.3.1":
"integrity" "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA=="
"resolved" "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
@@ -9012,6 +9024,17 @@
"prop-types" "^15.8.1"
"react-native-iphone-x-helper" "^1.3.1"
+"react-native-gesture-handler@^2.7.0":
+ "integrity" "sha512-0jr3FNm2R3gv/v6XTtENgjv0fewD6LEct8EWmXw/oHw36M3YiIIpxnW57thL+0YiKwyLBXN0QHL4JZbs/heW2Q=="
+ "resolved" "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.7.0.tgz"
+ "version" "2.7.0"
+ dependencies:
+ "@egjs/hammerjs" "^2.0.17"
+ "hoist-non-react-statics" "^3.3.0"
+ "invariant" "^2.2.4"
+ "lodash" "^4.17.21"
+ "prop-types" "^15.7.2"
+
"react-native-gradle-plugin@^0.70.3":
"integrity" "sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A=="
"resolved" "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz"