From 918e20a72b23d43bf372c42477a17f545113298a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Mon, 4 Sep 2023 09:59:48 +0200 Subject: [PATCH] SYNC 15 Major update, Formularies and Fixes - Add type Definitions - Fix models methods - Sync controller add route names - Formularies sync OK - Closes https://lab.s-web.fr/ldc-web/projets/com-app-bo/-/issues/18 - Closes https://lab.s-web.fr/ldc-web/projets/com-app/-/issues/9 --- ComApp/CHANGELOG.md | 2 +- ComApp/package-lock.json | 153 +- ComApp/package.json | 4 + ComApp/src/App.js | 2 +- ComApp/src/App/Controllers/SyncController.js | 106 +- ComApp/src/App/Enums/InputStatus.js | 17 + ComApp/src/App/Enums/InputType.js | 25 + ComApp/src/App/Lib/Helpers.js | 22 - ComApp/src/App/Models/Contact.js | 227 ++- ComApp/src/App/Models/Directory.js | 86 +- ComApp/src/App/Models/Document.js | 75 +- ComApp/src/App/Models/Form.js | 107 ++ ComApp/src/App/Models/SendBrochure.js | 56 +- ComApp/src/App/Models/Setting.js | 48 +- ComApp/src/App/Models/User.js | 61 +- ComApp/src/App/Store/Actions/actionType.js | 5 +- ComApp/src/App/Store/Actions/contacts.js | 48 +- ComApp/src/App/Store/Actions/directories.js | 11 +- ComApp/src/App/Store/Actions/documents.js | 10 +- ComApp/src/App/Store/Actions/formularies.js | 16 + ComApp/src/App/Store/Actions/locales.js | 14 +- ComApp/src/App/Store/Actions/users.js | 8 +- ComApp/src/App/Store/ConfigureStore.js | 4 +- ComApp/src/App/Store/Reducers/AppReducer.js | 40 +- ComApp/src/App/Store/Reducers/contacts.js | 29 +- ComApp/src/App/Store/Reducers/directories.js | 16 +- ComApp/src/App/Store/Reducers/documents.js | 12 +- ComApp/src/App/Store/Reducers/formularies.js | 30 + ComApp/src/App/Store/Reducers/locales.js | 24 +- ComApp/src/App/Store/Reducers/users.js | 14 +- ComApp/src/App/Views/ContactDetailsScreen.js | 1597 ++++++++++++------ ComApp/src/App/Views/ContactScreen.js | 253 ++- ComApp/src/App/Views/HomeScreen.js | 23 +- ComApp/src/App/Views/InstallScreen.js | 3 - ComApp/src/App/Views/LoginScreen.js | 76 +- ComApp/src/App/Views/SyncScreen.js | 266 ++- ComApp/src/Database.js | 15 +- ComApp/src/Resources/Locales/de.json | 238 +-- ComApp/src/Resources/Locales/en.json | 238 +-- ComApp/src/Resources/Locales/fr.json | 246 +-- ComApp/yarn.lock | 54 +- 41 files changed, 2929 insertions(+), 1352 deletions(-) create mode 100644 ComApp/src/App/Enums/InputStatus.js create mode 100644 ComApp/src/App/Enums/InputType.js create mode 100644 ComApp/src/App/Models/Form.js create mode 100644 ComApp/src/App/Store/Actions/formularies.js create mode 100644 ComApp/src/App/Store/Reducers/formularies.js diff --git a/ComApp/CHANGELOG.md b/ComApp/CHANGELOG.md index 5a0cb4f..14d1da3 100644 --- a/ComApp/CHANGELOG.md +++ b/ComApp/CHANGELOG.md @@ -11,4 +11,4 @@ - Mise en place de vérification par regex des champs obligatoires sur le formulaire de contact. - Ajout d'un bouton dans le header de la home pour aller directement sur la page d'ajout de contact. - Ajout de la possibilité de revenir à l'écran d'accueil en cliquant sur le logo présent dans le header. -- Ajout d'un espace plus important entre les éléments cliquables du header + ajout d'un menu en overlay. \ No newline at end of file +- Ajout d'un espace plus important entre les éléments cliquables du header + ajout d'un menu en overlay. diff --git a/ComApp/package-lock.json b/ComApp/package-lock.json index 422a94e..d82779a 100644 --- a/ComApp/package-lock.json +++ b/ComApp/package-lock.json @@ -17,6 +17,7 @@ "@rneui/themed": "^0.0.0-edge.2", "@sentry/react-native": "^4.12.0", "axios": "^0.27.2", + "buffer": "^6.0.3", "expo": "^47.0.10", "expo-av": "^13.0.2", "expo-barcode-scanner": "^11.4.0", @@ -31,13 +32,16 @@ "react-native-animated-tree-view": "^0.1.4", "react-native-b-crypt-bridge": "file:react-native-b-crypt-bridge-2.1.0.tgz", "react-native-blob-util": "^0.16.2", + "react-native-email-link": "^1.14.6", "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", "react-native-screens": "^3.18.0", + "react-native-sectioned-multi-select": "^0.10.0", "react-native-svg": "^13.2.0", + "react-native-uuid": "^2.0.1", "react-native-vector-icons": "^9.2.0", "react-redux": "^8.0.4", "realm": "^11.0.0-rc.2", @@ -6087,6 +6091,29 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bl/node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -6345,7 +6372,7 @@ "node": ">=6.9.0" } }, - "node_modules/buffer": { + "node_modules/bson/node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -6363,12 +6390,34 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -12308,6 +12357,11 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -17361,6 +17415,15 @@ "nullthrows": "^1.1.1" } }, + "node_modules/react-native-email-link": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/react-native-email-link/-/react-native-email-link-1.14.6.tgz", + "integrity": "sha512-XsWgdSMgxutKrm6WJRLKd1DxQpFt2x6xLuIhmAE5CdMMjNKadsUM5Evub9+otTBhlphSerePAwzSaNoBXpisLg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-native": ">=0.40.0" + } + }, "node_modules/react-native-flash-message": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/react-native-flash-message/-/react-native-flash-message-0.3.1.tgz", @@ -17459,6 +17522,23 @@ "react-native": "*" } }, + "node_modules/react-native-sectioned-multi-select": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-native-sectioned-multi-select/-/react-native-sectioned-multi-select-0.10.0.tgz", + "integrity": "sha512-wD5USgM88cHNkgQLB94NxbubtYCLacgzk2vfFlF+HvOI7noDz1AZfzu2sVp1WKz4bFx20MFxM4mi1uZoyxAutg==", + "dependencies": { + "lodash.isequal": "^4.5.0", + "memoize-one": "^6.0.0" + }, + "peerDependencies": { + "prop-types": "^15.6.0" + } + }, + "node_modules/react-native-sectioned-multi-select/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/react-native-size-matters": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.4.0.tgz", @@ -17480,6 +17560,15 @@ "react-native": "*" } }, + "node_modules/react-native-uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.1.tgz", + "integrity": "sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g==", + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/react-native-vector-icons": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", @@ -25233,6 +25322,15 @@ "readable-stream": "^3.4.0" }, "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -25419,15 +25517,26 @@ "integrity": "sha512-Uu4OCZa0jouQJCKOk1EmmyqtdWAP5HVLru4lQxTwzJzxT+sJ13lVpEZU/MATDxtHiekWMAL84oQY3Xn1LpJVSg==", "requires": { "buffer": "^5.6.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } } }, "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "requires": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "buffer-alloc": { @@ -29743,6 +29852,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -33389,6 +33503,12 @@ "nullthrows": "^1.1.1" } }, + "react-native-email-link": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/react-native-email-link/-/react-native-email-link-1.14.6.tgz", + "integrity": "sha512-XsWgdSMgxutKrm6WJRLKd1DxQpFt2x6xLuIhmAE5CdMMjNKadsUM5Evub9+otTBhlphSerePAwzSaNoBXpisLg==", + "requires": {} + }, "react-native-flash-message": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/react-native-flash-message/-/react-native-flash-message-0.3.1.tgz", @@ -33461,6 +33581,22 @@ "warn-once": "^0.1.0" } }, + "react-native-sectioned-multi-select": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-native-sectioned-multi-select/-/react-native-sectioned-multi-select-0.10.0.tgz", + "integrity": "sha512-wD5USgM88cHNkgQLB94NxbubtYCLacgzk2vfFlF+HvOI7noDz1AZfzu2sVp1WKz4bFx20MFxM4mi1uZoyxAutg==", + "requires": { + "lodash.isequal": "^4.5.0", + "memoize-one": "^6.0.0" + }, + "dependencies": { + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + } + } + }, "react-native-size-matters": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.4.0.tgz", @@ -33476,6 +33612,11 @@ "css-tree": "^1.1.3" } }, + "react-native-uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.1.tgz", + "integrity": "sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g==" + }, "react-native-vector-icons": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", diff --git a/ComApp/package.json b/ComApp/package.json index b9fe0c1..5ff20e8 100644 --- a/ComApp/package.json +++ b/ComApp/package.json @@ -19,6 +19,7 @@ "@rneui/themed": "^0.0.0-edge.2", "@sentry/react-native": "^4.12.0", "axios": "^0.27.2", + "buffer": "^6.0.3", "expo": "^47.0.10", "expo-av": "^13.0.2", "expo-barcode-scanner": "^11.4.0", @@ -33,13 +34,16 @@ "react-native-animated-tree-view": "^0.1.4", "react-native-b-crypt-bridge": "file:react-native-b-crypt-bridge-2.1.0.tgz", "react-native-blob-util": "^0.16.2", + "react-native-email-link": "^1.14.6", "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", "react-native-screens": "^3.18.0", + "react-native-sectioned-multi-select": "^0.10.0", "react-native-svg": "^13.2.0", + "react-native-uuid": "^2.0.1", "react-native-vector-icons": "^9.2.0", "react-redux": "^8.0.4", "realm": "^11.0.0-rc.2", diff --git a/ComApp/src/App.js b/ComApp/src/App.js index 7fc7bb3..bd71899 100644 --- a/ComApp/src/App.js +++ b/ComApp/src/App.js @@ -43,6 +43,7 @@ import SearchScreen from './App/Views/SearchScreen'; import SyncScreen from './App/Views/SyncScreen'; import VideoScreen from './App/Views/VideoScreen'; // Style +import Helpers from './App/Lib/Helpers'; import styles from './Resources/Styles/NavigationStyle'; // Controllers import SyncController from './App/Controllers/SyncController'; @@ -88,7 +89,6 @@ class App extends React.Component { isLogged: store.getState().app.logged, mainColor: store.getState().app.mainColor, }); // to force re-rendering - console.log('state changed'); } navigationView = () => ( diff --git a/ComApp/src/App/Controllers/SyncController.js b/ComApp/src/App/Controllers/SyncController.js index b9882bd..c96e3cd 100644 --- a/ComApp/src/App/Controllers/SyncController.js +++ b/ComApp/src/App/Controllers/SyncController.js @@ -7,13 +7,14 @@ import NetInfo from '@react-native-community/netinfo'; import * as Application from 'expo-application'; // Database import { Contact, Document, SendBrochure, Setting } from '../../Database'; +import { ContactData } from '../Models/Contact'; const api_token = 'G9Q62jt8eL8gN4Ls'; class SyncController { getUrlBo() { const test = Setting.get('bo_url'); - if (test === false || test === undefined) { + if (test === null) { return 'noset'; } return test.value; @@ -21,7 +22,7 @@ class SyncController { getAppStoreUUID() { const test = Setting.get('AppStoreUUID'); - if (test === false || test === undefined) { + if (test === null) { return 'noset'; } return test.value; @@ -34,6 +35,7 @@ class SyncController { async getVersion() { return axios({ method: 'GET', + // * version url: this.getUrlBo() + '/api/version', responseType: 'json', params: { @@ -47,6 +49,7 @@ class SyncController { async getUsers() { return axios({ method: 'GET', + // * users.index url: this.getUrlBo() + '/api/users', responseType: 'json', params: { @@ -55,18 +58,68 @@ class SyncController { }); } - async getContacts(userId) { - let contacts = Contact.getAll(); + /** + * @param {number} app_user_id + */ + async getContacts(app_user_id) { + // * We retrieve all contacts avec upsert and destroy + return axios({ + method: 'GET', + // * contacts.index + url: this.getUrlBo() + `/api/contacts/${app_user_id}`, + responseType: 'json', + params: { + token: api_token, + }, + }); + } + + /** + * @param {number} app_user_id + */ + async upsertContacts(app_user_id) { + const contacts = Contact.getAll(app_user_id).map(contact => { + // * This casts contact to a simple Object + // * Realm object does not support changing prop type + /** @type {ContactData} */ + contact = contact.toJSON(); + // * Convert to UTC timestamp + contact.timestamp = contact.timestamp.toUTCString(); + + // * Unstringify complex Fields + contact.data = JSON.parse(contact.data); + contact.visits = JSON.parse(contact.visits); + return contact; + }); return axios({ method: 'POST', - url: this.getUrlBo() + '/api/contacts', + // * contacts.index + url: this.getUrlBo() + `/api/contacts/${app_user_id}`, responseType: 'json', params: { token: api_token, }, data: { contacts: contacts, - userId: userId, + }, + }); + } + + /** + * @param {number} app_user_id + */ + async deleteContacts(app_user_id) { + const contactsToDelete = Contact.getAllToDelete(app_user_id); + return axios({ + method: 'DELETE', + // * contacts.index + url: this.getUrlBo() + `/api/contacts/${app_user_id}`, + responseType: 'json', + params: { + token: api_token, + }, + data: { + contactUuidList: contactsToDelete.map(contact => contact.id), }, }); } @@ -74,6 +127,7 @@ class SyncController { async sendBrochure(brochure) { return axios({ method: 'POST', + // * send-flyers url: this.getUrlBo() + '/api/sendBrochure', responseType: 'json', params: { @@ -86,6 +140,7 @@ class SyncController { async getDirectories() { return axios({ method: 'GET', + // * directories.index url: this.getUrlBo() + '/api/directories', responseType: 'json', params: { @@ -97,6 +152,7 @@ class SyncController { async getDocuments() { return axios({ method: 'GET', + // * documents.index url: this.getUrlBo() + '/api/documents', responseType: 'json', params: { @@ -105,13 +161,25 @@ class SyncController { }); } + async getFormularies() { + return axios({ + method: 'GET', + // * formularies.index + url: this.getUrlBo() + '/api/formularies', + responseType: 'json', + params: { + token: api_token, + }, + }); + } + updateLastModificationDoc(id, date) { Document.update({ id: id, updated_at: date.toString() }, true); } updateVersion(version) { const localVersion = Setting.get('version'); - if (localVersion === false || localVersion === undefined) { + if (localVersion === null) { Setting.add({ key: 'version', value: version }); } else { Setting.update({ id: localVersion.id, key: 'version', value: version }); @@ -120,16 +188,20 @@ class SyncController { updateShowIsActive(value) { const local = Setting.get('ShowIsActive'); - if (local === false || local === undefined) { + if (local === null) { Setting.add({ key: 'ShowIsActive', value: value }); } else { Setting.update({ id: local.id, key: 'ShowIsActive', value: value }); } } - isSynced() { + /** + * @param {number} app_user_id + * @returns + */ + isSynced(app_user_id) { const brochures = SendBrochure.getAll(); - const Contacts = Contact.getAll(); + const Contacts = Contact.getAll(app_user_id); if (brochures !== undefined && Contacts !== undefined) { const noSynced = Contacts.filtered('synced = false'); @@ -143,12 +215,7 @@ class SyncController { isLogged() { const logged = Setting.get('isLogged'); const loggedTime = Setting.get('loggedTime'); - if ( - logged !== false && - loggedTime !== false && - logged !== undefined && - loggedTime !== undefined - ) { + if (logged !== null && loggedTime !== null) { let seconds = Math.abs(new Date(Number(loggedTime.value)) - Date.now()) / 1000; if (seconds < 14400) { @@ -174,8 +241,7 @@ class SyncController { .then(response => { const localVersion = Setting.get('version'); if ( - localVersion === undefined || - localVersion === false || + localVersion === null || localVersion.value !== response.data.version ) { return callback(); @@ -183,7 +249,7 @@ class SyncController { return false; }) .catch(function (error) { - console.log(error); + console.error(error); }); } return false; @@ -201,7 +267,7 @@ class SyncController { isInstalled() { const test = Setting.get('installed'); - if (test === false || test === undefined) { + if (test === null) { return false; } return true; diff --git a/ComApp/src/App/Enums/InputStatus.js b/ComApp/src/App/Enums/InputStatus.js new file mode 100644 index 0000000..f3e6960 --- /dev/null +++ b/ComApp/src/App/Enums/InputStatus.js @@ -0,0 +1,17 @@ +/** InputStatusEnum */ + +/** + * @typedef {keyof InputStatusKeys} InputStatus + */ +export const InputStatus = { + SYSTEM: 1, + LOCKED: 2, + DEFAULT: 3, +}; + +// eslint-disable-next-line no-unused-vars +const InputStatusKeys = { + 1: 'SYSTEM', + 2: 'LOCKED', + 3: 'DEFAULT', +}; diff --git a/ComApp/src/App/Enums/InputType.js b/ComApp/src/App/Enums/InputType.js new file mode 100644 index 0000000..5f82b6d --- /dev/null +++ b/ComApp/src/App/Enums/InputType.js @@ -0,0 +1,25 @@ +/** InputTypeEnum */ + +/** + * @typedef {keyof InputTypeKeys} InputType + */ +export const InputType = { + TEXT: 1, + EMAIL: 2, + PHONE: 3, + RADIO: 4, + SELECT: 5, + SELECT_MULTIPLE: 6, + CHECKBOX: 7, +}; + +// eslint-disable-next-line no-unused-vars +const InputTypeKeys = { + 1: 'TEXT', + 2: 'EMAIL', + 3: 'PHONE', + 4: 'RADIO', + 5: 'SELECT', + 6: 'SELECT_MULTIPLE', + 7: 'CHECKBOX', +}; diff --git a/ComApp/src/App/Lib/Helpers.js b/ComApp/src/App/Lib/Helpers.js index a7a52ec..1397bee 100644 --- a/ComApp/src/App/Lib/Helpers.js +++ b/ComApp/src/App/Lib/Helpers.js @@ -29,27 +29,5 @@ class Helpers { value.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/App/Models/Contact.js b/ComApp/src/App/Models/Contact.js index 1f0d6ef..7699370 100644 --- a/ComApp/src/App/Models/Contact.js +++ b/ComApp/src/App/Models/Contact.js @@ -1,106 +1,195 @@ +import { default as UuidGenerator } from 'react-native-uuid'; +import { UpdateMode } from 'realm'; +import { InputType } from './../Enums/InputType'; + +/** + * @typedef {typeof ContactData} ContactData + */ +export const ContactData = { + /** @type {string|undefined} */ + id: undefined, + /** @type {string} */ + email: undefined, + /** @type {string|null} */ + lastname: null, + /** @type {string|null} */ + firstname: null, + /** @type {string|null} */ + phone: null, + /** @type {Array} */ + data: [], + /** @type {string|null} */ + notes: null, + /** @type {Array>|null} */ + visits: null, + /** @type {Date} */ + timestamp: null, + /** @type {number} The app user id */ + app_user_id: undefined, + /** @type {number|null} The app user id that should receive this contact on sync */ + send_to_app_user_id: null, + /** @type {boolean} If this model is to delete */ + to_delete: false, + /** @type {boolean} This model need sync */ + synced: true, +}; + export 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 }, + ...ContactData, + ...{ + id: 'string', + email: 'string', + lastname: { type: 'string', optional: true }, + firstname: { type: 'string', optional: true }, + phone: { type: 'string', optional: true }, + /** Json encoded string of ContactData.data */ + data: { type: 'string', default: '[]' }, + notes: { type: 'string', optional: true }, + /** Json encoded string of ContactData.visists */ + visits: { type: 'string', default: '[]' }, + timestamp: 'date', + send_to_app_user_id: { type: 'int', optional: true, default: null }, + app_user_id: 'int', + to_delete: { type: 'bool', default: false }, + synced: { type: 'bool', default: true }, + }, }, }; +/** + * @typedef {typeof InputData} InputDataType + */ +export const InputData = { + title: '', + formSlug: '', + /** @type {import('./../Enums/InputType').InputType} */ + type: InputType.TEXT, + /** @type {string|Array} */ + value: '', + required: false, +}; + +/** + * @typedef {Unmanaged & import('realm').Object} Contact + */ + class ContactModel { + /** + * ContactModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'Contacts'; + /** @type {Realm} */ this.realm = realm; } - add(data) { - data.uniq_id = data.id; - data.id = undefined; + /** @return {Unmanaged} */ + new() { + return Object.assign({}, ContactData); + } + + /** + * Set contact + * ! We can use only set since all contacts are dropped and redownloaded on sync + * @param {Contact} data + */ + set(data) { + const isNew = typeof data.id !== 'string' || !data.id?.length; + if (isNew) { + console.warn('is new', data); + // * Set Local UUID until it get synced + //* it will be ignore and new one will be generated by BO + data.id = UuidGenerator.v4(); + } this.realm.write(() => { - if (data.id === undefined) { - let id = this.realm.objects(this.schema).max('id'); - data.id = id === undefined ? 1 : id + 1; - } try { - this.realm.create(this.schema, data, 'never'); + this.realm.create( + this.schema, + data, + isNew ? UpdateMode.Never : UpdateMode.Modified, + ); } catch (e) { - console.log('db setting add', e); + console.error('db contact set', e); } }); } - upsert(data) { - let tmp = this.get(data.key); - if (tmp === false || tmp === undefined) { - this.add(data); - } else { - if (data.id === undefined) { - data.id = tmp.id; - } - this.realm.write(() => { - this.realm.create(this.schema, data, 'modified'); - }); - } - } - - update(data) { - this.realm.write(() => { - this.realm.create(this.schema, data, 'modified'); - }); - } - - get(id) { - let tmp = this.realm.objects(this.schema); + /** + * get contact with uuid + * @param {number} app_user_id App User id + * @param {string} uuid + * @returns {Contact?} + */ + get(app_user_id, uuid) { + /** @type {import('realm').Results} */ + const tmp = this.realm + .objects(this.schema) + .filtered(`app_user_id = ${app_user_id}`); if (tmp.length === 0) { - return false; - } else { - tmp = tmp.filtered('id = ' + id)[0]; + return null; } - return tmp; + return tmp.filtered(`id = '${uuid}'`)[0] ?? null; } - getByUser(userId) { - let tmp = this.realm.objects(this.schema); - tmp = tmp.filtered('to_delete = false AND user_id = ' + userId); - return tmp; + /** + * @param {number} app_user_id App User id + * @returns {import('realm').Results} + */ + getAll(app_user_id) { + return this.realm + .objects(this.schema) + .filtered(`to_delete = false AND app_user_id = ${app_user_id}`); } - getAll() { - let tmp = this.realm.objects(this.schema); - return tmp; + /** + * @param {number} app_user_id App User id + * @returns {import('realm').Results} + */ + getAllEditable(app_user_id) { + return this.realm + .objects(this.schema) + .filtered( + `to_delete = false AND send_to_app_user_id = null AND app_user_id = ${app_user_id}`, + ); } - clear() { - this.realm.write(() => { - this.realm.delete(this.getAll()); - }); + /** + * @param {number} app_user_id App User id + * @returns {import('realm').Results} + */ + getAllToDelete(app_user_id) { + return this.realm + .objects(this.schema) + .filtered(`to_delete = true AND app_user_id = ${app_user_id}`); } - delete(key) { - const tmp = this.get(key); - if (typeof tmp === 'object') { + /** + * delete contact with id + * @param {number} app_user_id App User id + * @param {string} uuid + */ + delete(app_user_id, uuid) { + const contact = this.get(app_user_id, uuid); + if (typeof contact === 'object') { this.realm.write(() => { - this.realm.delete(tmp); + this.realm.delete(contact); }); } } + + /** + * @param {number} app_user_id App User id + */ + clear(app_user_id) { + this.realm.write(() => { + this.realm.delete(this.getAll(app_user_id)); + }); + } } export default ContactModel; diff --git a/ComApp/src/App/Models/Directory.js b/ComApp/src/App/Models/Directory.js index bfa0baf..77565a4 100644 --- a/ComApp/src/App/Models/Directory.js +++ b/ComApp/src/App/Models/Directory.js @@ -2,52 +2,87 @@ export const DirectoriesSchema = { name: 'Directories', primaryKey: 'id', properties: { + /** @type {number} */ id: 'int', name: 'string', - parent: {type: 'int', optional: true}, + /** @type {number|null} */ + parent: { type: 'int', optional: true }, + /** @type {number} */ order: 'int', + /** @type {number} */ needAuthorization: 'int', }, }; + +/** + * @typedef {Unmanaged & import('realm').Object} Directory + */ + class DirectoryModel { + /** + * DirectoryModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'Directories'; + /** @type {Realm} */ this.realm = realm; } + /** + * @param {Directory} data + */ add(data) { this.realm.write(() => { this.realm.create(this.schema, data); }); } + /** + * @param {Directory} data + */ update(data) { this.realm.write(() => { this.realm.create(this.schema, data, 'modified'); }); } + /** + * get directory with id + * @param {number} id + * @returns {Directory?} + */ get(id) { - let tmp = this.realm.objects(this.schema); + /** @type {import('realm').Results} */ + const tmp = this.realm.objects(this.schema); if (tmp.length === 0) { - return false; - } else { - tmp = tmp.filtered('id = ' + id)[0]; + return null; } - return tmp; + return tmp.filtered('id = ' + id)[0] ?? null; } - getAll(role, authorizedFolders, format, lang) { - if (authorizedFolders !== null && authorizedFolders !== undefined) { - authorizedFolders = JSON.parse(authorizedFolders); - } + /** + * @param {string} role + * @param {string} authorizedDirs JSON array of {Directory} + * @param {boolean} format + * @param {string} lang + * @returns {import('realm').Results} + */ + getAll(role, authorizedDirs, format, lang) { + /** @type {Array} */ + const allowedDirs = + authorizedDirs !== null && authorizedDirs !== undefined + ? JSON.parse(authorizedDirs) + : []; + /** @type {import('realm').Results} */ let tmp = this.realm.objects(this.schema); if (role === 'USER') { - if (authorizedFolders.length > 0) { + if (allowedDirs.length > 0) { tmp = tmp.filtered( 'needAuthorization = 0 OR ' + - authorizedFolders - .map(folder => ' id = "' + folder.id + '"') + allowedDirs + .map(directory => ' id = "' + directory.id + '"') .join(' OR '), ); } else { @@ -60,18 +95,28 @@ class DirectoryModel { return tmp; } + /** Delete all */ clear() { this.realm.write(() => { this.realm.delete(this.realm.objects(this.schema)); }); } + /** + * Recursive Dir filter + * + * @param {import('realm').Results} list + * @param {string} lang + * @param {DirectoryData} parent + * @returns {DirectoryData[]} + */ _recursiveDir(list, lang, parent = null) { - let root = list.filter(e => e.parent === parent); - let tmp = []; + const root = list.filter(e => e.parent === parent); + const 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); + const childs = this._recursiveDir(list, lang, root[i].id); + /** @type {Object} */ + const name = JSON.parse(root[i].name); tmp[i] = {}; tmp[i].id = root[i].id; tmp[i].name = this.getI18nName(lang, name); @@ -87,7 +132,14 @@ class DirectoryModel { return tmp; } + /** + * Get i18n name + * @param {string} lang + * @param {Record} name + * @returns {string} + */ getI18nName(lang, name) { + /** @type {string} */ let tmp; switch (lang) { case 'fr': diff --git a/ComApp/src/App/Models/Document.js b/ComApp/src/App/Models/Document.js index 63e767d..1d95579 100644 --- a/ComApp/src/App/Models/Document.js +++ b/ComApp/src/App/Models/Document.js @@ -2,33 +2,52 @@ export const DocumentsSchema = { name: 'Documents', primaryKey: 'id', properties: { + /** @type {number} */ id: 'int', uuid: 'string', title: 'string', type: 'string', path: 'string', + /** @type {number} */ directory: 'int', lang: 'string', updated_at: 'string', }, }; +/** + * @typedef {Unmanaged & import('realm').Object} Document + */ + class DocumentModel { + /** + * DocumentModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'Documents'; + /** @type {Realm} */ this.realm = realm; } - add(document) { + /** + * @param {Document} data + */ + add(data) { this.realm.write(() => { - this.realm.create(this.schema, document, 'never'); + this.realm.create(this.schema, data, 'never'); }); } - update(document, updatedAtChange = false) { - let tmp = this.get(document.id); - if (tmp === undefined) { - this.add(document); + /** + * @param {Document} data + * @param {boolean} updatedAtChange + */ + update(data, updatedAtChange = false) { + const tmp = this.get(data.id); + if (tmp === null) { + this.add(data); } else { this.realm.write(() => { if (updatedAtChange === false) { @@ -36,27 +55,41 @@ class DocumentModel { this.schema, { id: tmp.id, - uuid: document.uuid, - title: document.title, - type: document.type, - path: document.path, - directory: document.directory, + uuid: data.uuid, + title: data.title, + type: data.type, + path: data.path, + directory: data.directory, }, 'modified', ); } else { - this.realm.create(this.schema, document, 'modified'); + this.realm.create(this.schema, data, 'modified'); } }); } } + /** + * get document with id + * @param {number} id + * @returns {Document?} + */ get(id) { - let tmp = this.realm.objects(this.schema).filtered('id = ' + id)[0]; - return tmp; + /** @type {Realm.Results} */ + const tmp = this.realm.objects(this.schema); + if (tmp.length === 0) { + return null; + } + return tmp.filtered('id = ' + id)[0] ?? null; } + /** + * @param {Array} authorizedDirs + * @returns {import('realm').Results} + */ getAll(authorizedDirs = false) { + /** @type {Realm.Results} */ let tmp = this.realm.objects(this.schema); if (authorizedDirs) { authorizedDirs = authorizedDirs.map(item => item.id); @@ -65,12 +98,22 @@ class DocumentModel { return tmp; } + /** + * @typedef { { uuid: typeof DocumentsSchema.properties.uuid, type: typeof DocumentsSchema.properties.type } } DocumentCleared + */ + + /** + * Delete all + * @param {Array} docs + * @param {Array} toDelete + * @returns {Array} + */ clear(docs, toDelete = []) { const localDocs = this.getAll(); localDocs.forEach(doc => { - let tmp = docs.filter(item => item.uuid === doc.uuid); + const tmp = docs.filter(item => item.uuid === doc.uuid); if (tmp.length === 0) { - toDelete.push({uuid: doc.uuid, type: doc.type}); + toDelete.push({ uuid: doc.uuid, type: doc.type }); this.realm.write(() => { this.realm.delete(doc); }); diff --git a/ComApp/src/App/Models/Form.js b/ComApp/src/App/Models/Form.js new file mode 100644 index 0000000..a201c4a --- /dev/null +++ b/ComApp/src/App/Models/Form.js @@ -0,0 +1,107 @@ +import { UpdateMode } from 'realm'; + +export const FormData = { + /** @type {number} */ + id: undefined, + /** @type {import('./../Enums/InputType').InputType} */ + type: undefined, + /** @type {string} */ + name: undefined, + /** @type {string} */ + slug: undefined, + + /** @type {import('realm').Dictionary} list of translated placeholders Map */ + placeholder: {}, + /** @type {import('realm').List} list of json encoded translated details, list of Map */ + details: [], + /** @type {boolean} */ + is_visible: false, + /** @type {boolean} */ + required: false, + /** @type {import('./../Enums/InputStatus').InputStatus} */ + status: 0, + /** @type {number} */ + order: 0, +}; + +export const FormsSchema = { + name: 'Formularies', + primaryKey: 'id', + properties: { + id: 'int', + type: 'int', + name: 'string', + slug: 'string', + placeholder: { type: 'dictionary', objectType: 'string' }, + details: { type: 'list', objectType: 'string' }, + is_visible: 'bool', + required: 'bool', + status: 'int', + order: 'int', + }, +}; + +/** + * @typedef {Unmanaged & import('realm').Object} Form + */ + +class FormModel { + /** + * DocumentModel + * @param {Realm} realm + */ + constructor(realm) { + /** @type string */ + this.schema = 'Formularies'; + /** @type {Realm} */ + this.realm = realm; + } + + /** + * @param {Form} data + */ + add(data) { + this.realm.write(() => { + this.realm.create(this.schema, data, UpdateMode.Never); + }); + } + + /** + * get document with id + * @param {number} id + * @returns {Form?} + */ + get(id) { + /** @type {Realm.Results
} */ + const tmp = this.realm.objects(this.schema); + if (tmp.length === 0) { + return null; + } + return tmp.filtered(`id = ${id}`)[0] ?? null; + } + + /** + * @param {import('./../Enums/InputStatus').InputStatus|null} status + * @returns {import('realm').Results} + */ + getAll(status = null) { + const objects = this.realm.objects(this.schema); + if (status !== null) { + return objects.filtered(`status = ${status}`); + } + return objects; + } + + /** + * @typedef { { uuid: typeof FormsSchema.properties.uuid, type: typeof FormsSchema.properties.type } } DocumentCleared + */ + + /** Delete all */ + clear() { + this.realm.write(() => { + this.realm.delete(this.getAll()); + }); + } +} + +export default FormModel; diff --git a/ComApp/src/App/Models/SendBrochure.js b/ComApp/src/App/Models/SendBrochure.js index 7203d11..c4aa7a3 100644 --- a/ComApp/src/App/Models/SendBrochure.js +++ b/ComApp/src/App/Models/SendBrochure.js @@ -2,21 +2,36 @@ export const SendBrochuresSchema = { name: 'SendBrochures', primaryKey: 'id', properties: { + /** @type {number} */ id: 'int', email: 'string', subject: 'string', message: 'string', documents: 'string', - user_id: 'int', + /** @type {number} */ + app_user_id: 'int', }, }; +/** + * @typedef {Unmanaged & import('realm').Object} SendBrochure + */ + class SendBrochureModel { + /** + * SendBrochureModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'SendBrochures'; + /** @type {Realm} */ this.realm = realm; } + /** + * @param {SendBrochure} data + */ add(data) { this.realm.write(() => { if (data.id === undefined) { @@ -31,9 +46,12 @@ class SendBrochureModel { }); } + /** + * @param {SendBrochure} data + */ upsert(data) { - let tmp = this.get(data.key); - if (tmp === false || tmp === undefined) { + const tmp = this.get(data.key); + if (tmp === null) { this.add(data); } else { if (data.id === undefined) { @@ -45,35 +63,49 @@ class SendBrochureModel { } } + /** + * @param {SendBrochure} data + */ update(data) { this.realm.write(() => { this.realm.create(this.schema, data, 'modified'); }); } + /** + * get sendBrochure with id + * @param {number} id + * @returns {SendBrochure?} + */ get(id) { - let tmp = this.realm.objects(this.schema); + /** @type {import('realm').Results} */ + const tmp = this.realm.objects(this.schema); if (tmp.length === 0) { - return false; - } else { - tmp = tmp.filtered('id = ' + id)[0]; + return null; } - return tmp; + return tmp.filtered('id = ' + id)[0] ?? null; } + /** + * @returns {import('realm').Results} + */ getAll() { - let tmp = this.realm.objects(this.schema); - return tmp; + return this.realm.objects(this.schema); } + /** Delete all */ clear() { this.realm.write(() => { this.realm.delete(this.getAll()); }); } - delete(key) { - const tmp = this.get(key); + /** + * delete sendBrochure with id + * @param {number} id id + */ + delete(id) { + const tmp = this.get(id); if (typeof tmp === 'object') { this.realm.write(() => { this.realm.delete(tmp); diff --git a/ComApp/src/App/Models/Setting.js b/ComApp/src/App/Models/Setting.js index a84d6ab..34507f7 100644 --- a/ComApp/src/App/Models/Setting.js +++ b/ComApp/src/App/Models/Setting.js @@ -2,22 +2,36 @@ export const SettingsSchema = { name: 'Settings', primaryKey: 'id', properties: { + /** @type {number} */ id: 'int', key: 'string', value: 'string', }, }; +/** + * @typedef {Unmanaged & import('realm').Object} Setting + */ + class SettingModel { + /** + * SettingModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'Settings'; + /** @type {Realm} */ this.realm = realm; } + /** + * @param {Setting} data + */ add(data) { this.realm.write(() => { if (data.id === undefined) { - let id = this.realm.objects(this.schema).max('id'); + const id = this.realm.objects(this.schema).max('id'); data.id = id === undefined ? 1 : id + 1; } try { @@ -28,9 +42,12 @@ class SettingModel { }); } + /** + * @param {Setting} data + */ upsert(data) { - let tmp = this.get(data.key); - if (tmp === false || tmp === undefined) { + const tmp = this.get(data.key); + if (tmp === null) { this.add(data); } else { if (data.id === undefined) { @@ -42,24 +59,35 @@ class SettingModel { } } + /** + * @param {Setting} data + */ update(data) { this.realm.write(() => { this.realm.create(this.schema, data, 'modified'); }); } + /** + * get setting with key + * @param {string} key + * @returns {Setting?} + */ get(key) { - let tmp = this.realm.objects(this.schema); + /** @type {import('realm').Results} */ + const tmp = this.realm.objects(this.schema); if (tmp.length === 0) { - return false; - } else { - tmp = tmp.filtered('key = "' + key + '"')[0]; + return null; } - return tmp; + return tmp.filtered('key = "' + key + '"')[0] ?? null; } - delete(key) { - const tmp = this.get(key); + /** + * delete setting with id + * @param {number} id id + */ + delete(id) { + const tmp = this.get(id); if (typeof tmp === 'object') { this.realm.write(() => { this.realm.delete(tmp); diff --git a/ComApp/src/App/Models/User.js b/ComApp/src/App/Models/User.js index c6f85f0..cc8651e 100644 --- a/ComApp/src/App/Models/User.js +++ b/ComApp/src/App/Models/User.js @@ -1,23 +1,55 @@ +/** + * @typedef {typeof UserData} UserData + */ +export const UserData = { + /** @type {string|undefined} */ + id: undefined, + /** @type {string} */ + surname: undefined, + /** @type {string|null} */ + lastname: null, + /** @type {string|null} */ + firstname: null, + /** @type {string|null} */ + role: null, + /** @type {string|null} */ + authorizedFolders: null, +}; export const UsersSchema = { name: 'Users', primaryKey: 'id', properties: { + /** @type {number} */ id: 'int', email: 'string', surname: 'string', firstname: 'string', password: 'string', role: 'string', + /** Json encoded string of {import('./Models/Directory').Directory[]} */ authorizedFolders: 'string', }, }; +/** + * @typedef {Unmanaged & import('realm').Object} User + */ + class UserModel { + /** + * UserModel + * @param {Realm} realm + */ constructor(realm) { + /** @type string */ this.schema = 'Users'; + /** @type {Realm} */ this.realm = realm; } + /** + * @param {User} data + */ add(data) { this.realm.write(() => { if (data.id === undefined) { @@ -32,8 +64,11 @@ class UserModel { }); } + /** + * @param {User} data + */ upsert(data) { - let tmp = this.get(data.key); + const tmp = this.get(data.key); if (tmp === false || tmp === undefined) { this.add(data); } else { @@ -46,27 +81,37 @@ class UserModel { } } + /** + * @param {User} data + */ update(data) { this.realm.write(() => { this.realm.create(this.schema, data, 'modified'); }); } + /** + * get user with id + * @param {number} id + * @returns {User?} + */ get(id) { - let tmp = this.realm.objects(this.schema); + /** @type {import('realm').Results} */ + const tmp = this.realm.objects(this.schema); if (tmp.length === 0) { - return false; - } else { - tmp = tmp.filtered('id = ' + id)[0]; + return null; } - return tmp; + return tmp.filtered('key = "' + id + '"')[0] ?? null; } + /** + * @returns {import('realm').Results} + */ getAll() { - let tmp = this.realm.objects(this.schema); - return tmp; + return this.realm.objects(this.schema); } + /** Delete all */ clear() { this.realm.write(() => { this.realm.delete(this.getAll()); diff --git a/ComApp/src/App/Store/Actions/actionType.js b/ComApp/src/App/Store/Actions/actionType.js index dd0f29a..59714c8 100644 --- a/ComApp/src/App/Store/Actions/actionType.js +++ b/ComApp/src/App/Store/Actions/actionType.js @@ -5,10 +5,11 @@ export const EDIT_LOCALE = 'EDIT_LOCALE'; export const INIT_USERS = 'INIT_USERS'; // Contacts export const INIT_CONTACT = 'INIT_CONTACT'; -export const ADD_CONTACT = 'ADD_CONTACT'; -export const EDIT_CONTACT = 'EDIT_CONTACT'; +export const SET_CONTACT = 'ADD_CONTACT'; export const DELETE_CONTACT = 'DELETE_CONTACT'; // Directories export const INIT_DIRECTORIES = 'INIT_DIRECTORIES'; // Documents export const INIT_DOCUMENTS = 'INIT_DOCUMENTS'; +// Formularies +export const INIT_FORMS = 'INIT_FORMS'; diff --git a/ComApp/src/App/Store/Actions/contacts.js b/ComApp/src/App/Store/Actions/contacts.js index 4fba842..7461a0d 100644 --- a/ComApp/src/App/Store/Actions/contacts.js +++ b/ComApp/src/App/Store/Actions/contacts.js @@ -1,14 +1,13 @@ -import { - ADD_CONTACT, - DELETE_CONTACT, - EDIT_CONTACT, - INIT_CONTACT, -} from './actionType'; +import { DELETE_CONTACT, INIT_CONTACT, SET_CONTACT } from './actionType'; + +/** + * @typedef { {currentUser: import('../../Models/User').User, data: import('../../Models/Contact').Contact} } InitContactAction + */ /** * Init contact - * @param {object} currentUser The currentUser - * @param {object} data The contact data + * @param {import('../../Models/User').User} currentUser The currentUser + * @param {import('../../Models/Contact').Contact} data The contact data */ export const initContact = (currentUser, data) => { return { @@ -19,40 +18,35 @@ export const initContact = (currentUser, data) => { }; /** - * Add contact - * @param {object} currentUser The currentUser - * @param {object} data The contact data + * @typedef { {currentUser: import('../../Models/User').User, data: import('../../Models/Contact').Contact} } SetContactAction */ -export const addContact = (currentUser, data) => { + +/** + * Add contact + * @param {import('../../Models/User').User} currentUser The currentUser + * @param {import('../../Models/Contact').Contact} data The contact data + */ +export const setContact = (currentUser, data) => { return { - type: ADD_CONTACT, + type: SET_CONTACT, currentUser, data, }; }; /** - * Edit contact - * @param {object} currentUser The currentUser - * @param {object} data The contact data + * @typedef { {currentUser: import('../../Models/User').User, uuid: string} } DeleteContactAction */ -export const editContact = (currentUser, data) => { - return { - type: EDIT_CONTACT, - currentUser, - data, - }; -}; /** * Delete contact - * @param {object} currentUser The currentUser - * @param {string} id The contact id + * @param {import('../../Models/User').User} currentUser The currentUser + * @param {string} uuid The contact uuid */ -export const deleteContact = (currentUser, id) => { +export const deleteContact = (currentUser, uuid) => { return { type: DELETE_CONTACT, currentUser, - id, + uuid: uuid, }; }; diff --git a/ComApp/src/App/Store/Actions/directories.js b/ComApp/src/App/Store/Actions/directories.js index 297d2a2..aaecb5f 100644 --- a/ComApp/src/App/Store/Actions/directories.js +++ b/ComApp/src/App/Store/Actions/directories.js @@ -1,10 +1,15 @@ -import {INIT_DIRECTORIES} from './actionType'; +import { INIT_DIRECTORIES } from './actionType'; + +/** + * @typedef { {type: typeof INIT_DIRECTORIES, lang: string, user: import('../../Models/User').User, data: import('../../Models/Directory').Directory} } InitDirectoryAction + */ /** * Init * @param {string} lang The lang - * @param {object} user User - * @param {object} data The document data + * @param {import('../../Models/User').User} user + * @param {import('../../Models/Directory').Directory} data The document data + * @returns {InitDirectoryAction} */ export const initDirectories = (lang, user, data) => { return { diff --git a/ComApp/src/App/Store/Actions/documents.js b/ComApp/src/App/Store/Actions/documents.js index 2cbb82b..5b96cd4 100644 --- a/ComApp/src/App/Store/Actions/documents.js +++ b/ComApp/src/App/Store/Actions/documents.js @@ -1,10 +1,14 @@ -import {INIT_DOCUMENTS} from './actionType'; +import { INIT_DOCUMENTS } from './actionType'; + +/** + * @typedef { {type: typeof INIT_DOCUMENTS, lang: string, directories: import('realm').Results, data: import('../../Models/Document').Document } } InitDocumentsAction + */ /** * Init contact * @param {string} lang The lang - * @param {object} directories Directories - * @param {object} data The document data + * @param {Realm.Results} directories Directories + * @param {import('../../Models/Document').Document} data The document data */ export const initDocuments = (lang, directories, data) => { return { diff --git a/ComApp/src/App/Store/Actions/formularies.js b/ComApp/src/App/Store/Actions/formularies.js new file mode 100644 index 0000000..499d261 --- /dev/null +++ b/ComApp/src/App/Store/Actions/formularies.js @@ -0,0 +1,16 @@ +import { INIT_FORMS } from './actionType'; + +/** + * @typedef { {type: typeof INIT_FORMS, data: import('../../Models/Form').Form } } InitFormAction + */ + +/** + * Init forms + * @param {import('../../Models/Form').Form} data The forms data + */ +export const initFormularies = data => { + return { + type: INIT_FORMS, + data, + }; +}; diff --git a/ComApp/src/App/Store/Actions/locales.js b/ComApp/src/App/Store/Actions/locales.js index ca87469..7b04aee 100644 --- a/ComApp/src/App/Store/Actions/locales.js +++ b/ComApp/src/App/Store/Actions/locales.js @@ -1,8 +1,12 @@ -import {EDIT_LOCALE, INIT_LOCALE} from './actionType'; +import { EDIT_LOCALE, INIT_LOCALE } from './actionType'; + +/** + * @typedef { {type: typeof INIT_LOCALE, data: string } } InitLocaleAction + */ /** * Init locale - * @param {object} data The locale data + * @param {string} data The locale data */ export const initLocale = data => { return { @@ -11,9 +15,13 @@ export const initLocale = data => { }; }; +/** + * @typedef { {type: typeof EDIT_LOCALE, data: string } } EditLocaleAction + */ + /** * Edit locale - * @param {object} data The locale data + * @param {string} data The locale data */ export const editLocale = data => { return { diff --git a/ComApp/src/App/Store/Actions/users.js b/ComApp/src/App/Store/Actions/users.js index a673d76..cd8fcaa 100644 --- a/ComApp/src/App/Store/Actions/users.js +++ b/ComApp/src/App/Store/Actions/users.js @@ -1,8 +1,12 @@ -import {INIT_USERS} from './actionType'; +import { INIT_USERS } from './actionType'; + +/** + * @typedef { {type: typeof INIT_USERS, data: import('../../Models/User').User } } InitUserAction + */ /** * Init users - * @param {object} data The users data + * @param {import('../../Models/User').User} data The users data */ export const initUsers = data => { return { diff --git a/ComApp/src/App/Store/ConfigureStore.js b/ComApp/src/App/Store/ConfigureStore.js index a686cfb..9a6b9a7 100644 --- a/ComApp/src/App/Store/ConfigureStore.js +++ b/ComApp/src/App/Store/ConfigureStore.js @@ -1,9 +1,10 @@ -import {combineReducers, createStore} from 'redux'; +import { combineReducers, createStore } from 'redux'; import appReducer from './Reducers/AppReducer'; import contactsReducer from './Reducers/contacts'; import directoriesReducer from './Reducers/directories'; import documentsReducer from './Reducers/documents'; +import formulariesReducer from './Reducers/formularies'; import localesReducer from './Reducers/locales'; import usersReducer from './Reducers/users'; @@ -14,6 +15,7 @@ const rootReducer = combineReducers({ directories: directoriesReducer, locale: localesReducer, users: usersReducer, + formularies: formulariesReducer, }); export const store = createStore(rootReducer); diff --git a/ComApp/src/App/Store/Reducers/AppReducer.js b/ComApp/src/App/Store/Reducers/AppReducer.js index 44e51cc..c6f471b 100644 --- a/ComApp/src/App/Store/Reducers/AppReducer.js +++ b/ComApp/src/App/Store/Reducers/AppReducer.js @@ -13,9 +13,6 @@ const initialState = { ShowIsActive: 0, HomeBtns: {}, mainColor: '#D4021D', - FormSelect1: {}, - FormSelect2: {}, - FormCheck: [], }; const reducer = (state = initialState, action) => { @@ -24,43 +21,22 @@ const reducer = (state = initialState, action) => { case 'INIT': let ShowIsActive = Setting.get('ShowIsActive'); let MainColor = Setting.get('MainColor'); - let FormSelect1 = Setting.get('FormSelect1'); - let FormSelect2 = Setting.get('FormSelect2'); - let FormCheck = Setting.get('FormCheck'); saved = Setting.get('isSetup'); return { ...state, isSetup: - saved === undefined ? false : saved.value === 'true' ? true : false, - ShowIsActive: ShowIsActive === undefined ? false : ShowIsActive.value, + saved === undefined ? false : saved?.value === 'true' ? true : false, + ShowIsActive: ShowIsActive ? ShowIsActive?.value : false, mainColor: MainColor === undefined ? initialState.mainColor - : MainColor === false + : MainColor === null ? initialState.mainColor - : MainColor.value, - FormSelect1: - FormSelect1 === undefined - ? initialState.FormSelect1 - : FormSelect1 === false - ? initialState.FormSelect1 - : JSON.parse(FormSelect1.value), - FormSelect2: - FormSelect2 === undefined - ? initialState.FormSelect2 - : FormSelect2 === false - ? initialState.FormSelect2 - : JSON.parse(FormSelect2.value), - FormCheck: - FormCheck === undefined - ? initialState.FormCheck - : FormCheck === false - ? initialState.FormCheck - : JSON.parse(FormCheck.value), + : MainColor?.value, }; case 'INIT_HOME_BTNS': { let HomeFolders = Setting.get('HomeFolders'); - let Btns = JSON.parse(HomeFolders.value); + let Btns = HomeFolders ? JSON.parse(HomeFolders?.value) : []; return { ...state, HomeBtns: Btns, @@ -72,7 +48,7 @@ const reducer = (state = initialState, action) => { return { ...state, isSetup: - saved === undefined ? false : saved.value === 'true' ? true : false, + saved === undefined ? false : saved?.value === 'true' ? true : false, }; case 'SET_CURRENT_USER': let user = false; @@ -80,8 +56,8 @@ const reducer = (state = initialState, action) => { if (action.data === false) { Setting.delete('isLogged'); } else { - userObj = Setting.get('isLogged').value; - user = JSON.parse(userObj); + let userObj = Setting.get('isLogged'); + user = userObj ? JSON.parse(userObj?.value) : false; isLogged = true; } return { diff --git a/ComApp/src/App/Store/Reducers/contacts.js b/ComApp/src/App/Store/Reducers/contacts.js index 9ec2fb2..11aeae0 100644 --- a/ComApp/src/App/Store/Reducers/contacts.js +++ b/ComApp/src/App/Store/Reducers/contacts.js @@ -1,31 +1,32 @@ import { - ADD_CONTACT, DELETE_CONTACT, - EDIT_CONTACT, INIT_CONTACT, + SET_CONTACT, } from '../Actions/actionType'; // Models -import {Contact} from '../../../Database'; +import { Contact } from '../../../Database'; +/** @type {import('../../Models/Contact').Contact[]} */ const initialState = []; +/** + * @param {typeof initialState} state + * @param {import('../Actions/contacts').InitContactAction|import('../Actions/contacts').SetContactAction|import('../Actions/contacts').DeleteContactAction} action + * @returns {import('realm').Results} + */ const reducer = (state = initialState, action) => { switch (action.type) { case INIT_CONTACT: { - return Contact.getByUser(action.currentUser.id); + return Contact.getAllEditable(action.currentUser.id); } - case ADD_CONTACT: { - action.data.user_id = action.currentUser.id; - Contact.add(action.data); - return Contact.getByUser(action.currentUser.id); - } - case EDIT_CONTACT: { - Contact.update(action.data); - return Contact.getByUser(action.currentUser.id); + case SET_CONTACT: { + action.data.app_user_id = action.currentUser.id; + Contact.set(action.data); + return Contact.getAllEditable(action.currentUser.id); } case DELETE_CONTACT: { - Contact.delete(action.id); - return Contact.getByUser(action.currentUser.id); + Contact.delete(action.currentUser.id, action.uuid); + return Contact.getAllEditable(action.currentUser.id); } default: { diff --git a/ComApp/src/App/Store/Reducers/directories.js b/ComApp/src/App/Store/Reducers/directories.js index 55f91a7..e489cc1 100644 --- a/ComApp/src/App/Store/Reducers/directories.js +++ b/ComApp/src/App/Store/Reducers/directories.js @@ -1,9 +1,19 @@ -import {INIT_DIRECTORIES} from '../Actions/actionType'; +import { Directory } from '../../../Database'; +import { INIT_DIRECTORIES } from '../Actions/actionType'; // Models -import {Directory} from '../../../Database'; -const initialState = []; +const initialState = { + /** @type {Realm.Results} */ + global: [], + /** @type {Realm.Results} */ + treeview: [], +}; +/** + * @param {typeof initialState} state + * @param {import('../Actions/directories').InitDirectoryAction} action + * @returns + */ const reducer = (state = initialState, action) => { switch (action.type) { case INIT_DIRECTORIES: { diff --git a/ComApp/src/App/Store/Reducers/documents.js b/ComApp/src/App/Store/Reducers/documents.js index 30a6ac3..c1a4a90 100644 --- a/ComApp/src/App/Store/Reducers/documents.js +++ b/ComApp/src/App/Store/Reducers/documents.js @@ -1,13 +1,19 @@ -import {INIT_DOCUMENTS} from '../Actions/actionType'; +import { INIT_DOCUMENTS } from '../Actions/actionType'; // Models -import {Document} from '../../../Database'; +import { Document } from '../../../Database'; +/** @type {import('../../Models/Document').Document[]} */ const initialState = []; +/** + * @param {typeof initialState} state + * @param {import('../Actions/documents').InitDocumentsAction} action + * @returns + */ const reducer = (state = initialState, action) => { switch (action.type) { case INIT_DOCUMENTS: { - let docs = Document.getAll(action.directories); + const docs = Document.getAll(action.directories); if (docs.length === 0) { return state; } diff --git a/ComApp/src/App/Store/Reducers/formularies.js b/ComApp/src/App/Store/Reducers/formularies.js new file mode 100644 index 0000000..261c56d --- /dev/null +++ b/ComApp/src/App/Store/Reducers/formularies.js @@ -0,0 +1,30 @@ +import { INIT_FORMS } from '../Actions/actionType'; +// Models +import { Form } from '../../../Database'; + +const initialState = { + /** @type {Realm.Results} */ + users: [], +}; + +/** + * @param {typeof initialState} state + * @param {import('../Actions/formularies').InitFormAction} action + * @returns + */ +const reducer = (state = initialState, action) => { + switch (action.type) { + case INIT_FORMS: { + return { + ...state, + forms: Form.getAll(), + }; + } + + default: { + return state; + } + } +}; + +export default reducer; diff --git a/ComApp/src/App/Store/Reducers/locales.js b/ComApp/src/App/Store/Reducers/locales.js index f5bedb3..7659c58 100644 --- a/ComApp/src/App/Store/Reducers/locales.js +++ b/ComApp/src/App/Store/Reducers/locales.js @@ -1,30 +1,32 @@ -import {EDIT_LOCALE, INIT_LOCALE} from '../Actions/actionType'; +import { EDIT_LOCALE, INIT_LOCALE } from '../Actions/actionType'; // Models -import {Setting} from '../../../Database'; +import { Setting } from '../../../Database'; // Translations import i18n from '../../../I18n'; -const initialState = []; +const initialState = { + locale: 'fr', +}; +const availableLocales = ['fr', 'en', 'de']; +/** + * @param {typeof initialState} state + * @param {import('../Actions/locales').InitLocaleAction|import('../Actions/locales').EditLocaleAction} action + * @returns + */ const reducer = (state = initialState, action) => { switch (action.type) { case INIT_LOCALE: { const savedLocale = Setting.get('locale'); return { ...state, - locale: - savedLocale === undefined - ? 'fr' - : savedLocale === false - ? 'fr' - : savedLocale.value, + locale: savedLocale === null ? initialState.locale : savedLocale.value, }; } case EDIT_LOCALE: { let nxt; if (action.data === undefined) { - const availableLocales = ['fr', 'en', 'de']; if ( availableLocales.indexOf(i18n.locale) === availableLocales.length - 1 @@ -37,7 +39,7 @@ const reducer = (state = initialState, action) => { nxt = action.data; } i18n.locale = nxt; - Setting.upsert({key: 'locale', value: nxt}); + Setting.upsert({ key: 'locale', value: nxt }); return { ...state, locale: nxt, diff --git a/ComApp/src/App/Store/Reducers/users.js b/ComApp/src/App/Store/Reducers/users.js index 0d7353c..a1e7476 100644 --- a/ComApp/src/App/Store/Reducers/users.js +++ b/ComApp/src/App/Store/Reducers/users.js @@ -1,9 +1,17 @@ -import {INIT_USERS} from '../Actions/actionType'; +import { INIT_USERS } from '../Actions/actionType'; // Models -import {User} from '../../../Database'; +import { User } from '../../../Database'; -const initialState = []; +const initialState = { + /** @type {Realm.Results} */ + users: [], +}; +/** + * @param {typeof initialState} state + * @param {import('../Actions/users').InitUserAction} action + * @returns + */ const reducer = (state = initialState, action) => { switch (action.type) { case INIT_USERS: { diff --git a/ComApp/src/App/Views/ContactDetailsScreen.js b/ComApp/src/App/Views/ContactDetailsScreen.js index 78c1af3..76982f9 100644 --- a/ComApp/src/App/Views/ContactDetailsScreen.js +++ b/ComApp/src/App/Views/ContactDetailsScreen.js @@ -3,6 +3,7 @@ import { Alert, Button, ScrollView, + StyleSheet, Text, TextInput, TouchableOpacity, @@ -10,7 +11,7 @@ import { } from 'react-native'; // Reduxs import { connect } from 'react-redux'; -import { addContact, editContact } from '../Store/Actions/contacts'; +import { setContact } from '../Store/Actions/contacts'; // Helpers import Helpers from '../Lib/Helpers'; // Translations @@ -25,10 +26,17 @@ import { ButtonGroup, CheckBox, Overlay } from '@rneui/themed'; import { showMessage } from 'react-native-flash-message'; // Picker import { Picker } from '@react-native-picker/picker'; +// * Select List +import SectionedMultiSelect from 'react-native-sectioned-multi-select'; // vCard parser import vCard from 'vcard-parser'; // BarCode Scanner import { BarCodeScanner } from 'expo-barcode-scanner'; +// FormModel +import { InputStatus } from '../Enums/InputStatus'; +import { InputType } from '../Enums/InputType'; +import { Contact, Form } from './../../Database'; +import { InputData } from './../Models/Contact'; function mapStateToProps(state) { return { @@ -36,172 +44,294 @@ function mapStateToProps(state) { contacts: state.contacts, currentUser: state.app.currentUser, mainColor: state.app.mainColor, - FormSelect1: state.app.FormSelect1, - FormSelect2: state.app.FormSelect2, - FormCheck: state.app.FormCheck, }; } +/** + * @typedef {(user: import('./../Models/User').User, data: import('./../Models/Contact').Contact) => any} setContact + */ + const mapDispatchToProps = dispatch => { return { - addContact: (user, data) => dispatch(addContact(user, data)), - editContact: (user, data) => dispatch(editContact(user, data)), + setContact: (user, data) => dispatch(setContact(user, data)), }; }; - -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 { + constructor(props) { + super(props); + + // * This is to hold array of number for multiple inputs selects + const multipleSelectInputs = Object.fromEntries( + new Map( + Form.getAll() + .filter(form => form.type === InputType.SELECT_MULTIPLE) + .map(form => [`input${form.id}`, { show: false, selectedItems: [] }]), + ), + ); + + const { contactId } = props.route.params; + + const paramContact = Contact.get(this.props.currentUser.id, contactId); + + if (paramContact) { + this.state.action = 'edit'; + } + + // * Init Contact data list + const contact = paramContact + ? this.initContact(paramContact) + : this.initNewContact(Contact.new()); + this.state.contact = contact; + + // * Setup All inputs State + this.state.allInputs = Form.getAll().map(form => { + const isDefault = form.status === InputStatus.DEFAULT; + const inputData = Object.assign({}, InputData); + inputData.title = form.placeholder[this.props.locale]; + inputData.required = form.required; + inputData.formSlug = form.slug; + inputData.type = form.type; + inputData.value = + (!isDefault + ? contact[form.slug] + : contact.data.find(input => input.formSlug === form.slug)?.value) ?? + ''; + if (form.type === InputType.CHECKBOX) { + inputData.value = + (!isDefault + ? contact[form.slug] + : contact.data.find(input => input.formSlug === form.slug) + ?.value) ?? []; + } + // * Init Multiple selete State + if (form.type === InputType.SELECT_MULTIPLE) { + const details = form.details.map( + detail => JSON.parse(detail)[i18n.locale] ?? null, + ); + multipleSelectInputs[`input${form.id}`].selectedItems = ( + inputData.value ?? [] + ) + .map((selectedDetail, detailIndex) => details.indexOf(selectedDetail)) + .filter(detailIndex => detailIndex !== -1); + console.debug('SELECT_MULTIPLE', inputData.value); + console.debug(multipleSelectInputs[`input${form.id}`]); + } + return inputData; + }); + this.state.multipleSelectInputs = multipleSelectInputs; + } + + // * --- STATE --- state = { + /** @type {'add'|'edit} */ action: 'add', - visits: [], - selectedVisit: 0, - tmpVisitNote: '', - newVisitTitle: date + '/' + month + '/' + year, - selectedIndex: 0, + + /** @type {0|1} 0 is VISITS 1, is NOTES */ + selectedVisitsOrNoteTab: 0, + + // * -- VISITS + selectedVisitIndex: 0, showNewVisit: false, - tmpID: 0, - tmpNote: '', - tmpName: '', - tmpFirstname: '', - tmpEmail: '', - tmpCompany: '', - tmpAddress: '', - tmpPhone: '', - tmpSelect1: '', - tmpSelect2: '', - tmpOther: '', - clientChecked: false, - prospectChecked: false, - otherChecked: false, - productsCheck: [], - showQRcodevCard: false, - parsedType: null, - parsedDatas: [], + tmpVisitNote: '', + newVisitTitle: '', + + // * -- VCARD + /** @type {boolean|null} TRUE ok, FALSE refused, NULL not asked */ hasPermission: null, - scanned: false, + vCardData: { + name: '', + firstname: '', + email: '', + company: '', + address: '', + phone: '', + }, + /** @type {string|null} */ + parsedType: null, + /** @type {string[]} */ + parsedDatas: [], + /** @type {boolean} */ + showQRcodevCard: false, + /** @type {boolean} */ showDynContact: false, - parsedName: '', - parsedFirstname: '', - parsedEmail: '', - parsedCompany: '', - parsedAddress: '', - parsedPhone: '', + /** @type {boolean} */ + scanned: false, }; - updateContact() { + // * --- METHODS + + // * - CONTACT METHODS + saveContact() { + /** @type {typeof import('./../Models/Contact').ContactData} */ + const contact = Object.assign({}, this.state.contact); + + const defaultForms = Form.getAll(InputStatus.DEFAULT); + + console.debug('save Contact'); + 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'); - } + + // * VALIDATE SYSTEM FIELDS if ( - this.state.tmpEmail.length < 5 || - !Helpers.isEmailValid(this.state.tmpEmail) + !contact.email || + contact.email.length < 5 || + contact.email.length >= 255 || + !Helpers.isEmailValid(contact.email) ) { + console.debug('invalid prop email'); 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 (contact.firstname && contact.firstname.length >= 255) { + console.debug('invalid prop invalid_firstname'); + error = error.concat(i18n.t('contact.invalid_firstname') + '.\n'); } - if (this.state.tmpFirstname.length === 0) { - error = error.concat(i18n.t('error.invalid_firstname') + '.\n'); + if (contact.lastname && contact.lastname.length >= 255) { + console.debug('invalid prop lastname'); + error = error.concat(i18n.t('contact.invalid_lastname') + '.\n'); } + if (contact.phone && contact.phone.length >= 255) { + console.debug('invalid prop phone'); + error = error.concat(i18n.t('contact.invalid_phone') + '.\n'); + } + + contact.data.forEach(input => { + const formToValidate = defaultForms.find( + form => form.slug === input.formSlug, + ); + if (formToValidate.required && !input?.value?.length) { + console.debug('invalid data ', input.formSlug); + error = error.concat( + i18n.t('contact_details_s.input_required', { + placeholder: formToValidate.placeholder[i18n.locale], + }) + '.\n', + ); + } + if ( + input?.value?.length && + formToValidate.type === InputType.EMAIL && + !Helpers.isEmailValid(input.value) + ) { + console.debug('invalid data email ', input.formSlug); + error = error.concat( + i18n.t('contact_details_s.input_email', { + placeholder: formToValidate.placeholder[i18n.locale], + }) + '.\n', + ); + } + }); if (error.length > 0) { Alert.alert(i18n.t('global.warning') + ' !', error, [{ text: 'OK' }], { cancelable: false, }); + return; + } + + console.debug(contact.data); + + // * Stringify fields to store them + contact.visits = JSON.stringify(contact.visits); + contact.data = JSON.stringify(contact.data); + contact.timestamp = new Date(); + contact.synced = false; + + /** @type {setContact} */ + const setContactHook = this.props.setContact; + + setContactHook(this.props.currentUser, contact); + + if (this.state.action === 'edit') { + showMessage({ + message: i18n.t('contact_details_s.contact_saved') + '.', + type: 'success', + duration: 500, + }); } else { - 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(this.state.productsCheck), - 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.editContact(this.props.currentUser, contact); - this.populate(); - showMessage({ - message: i18n.t('contact_details_s.contact_saved') + '.', - type: 'success', - duration: 500, + setTimeout(() => { + this.props.navigation.navigate('ContactScreen', { + contactAdded: true, }); - } else { - this.props.addContact(this.props.currentUser, contact); - this.props.navigation.navigate('ContactScreen', { contactAdded: true }); - } + }, 1500); } } - 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, - }); - } + /** + * + * @param {import('./../Models/Contact').Contact} contact + * @returns {import('./../Models/Contact').ContactData} + */ + initContact(contact) { + contact = contact.toJSON(); + // * Stringify fields to store them + contact.visits = JSON.parse(contact.visits); + /** @type {Map} */ + const originalData = new Map( + [...JSON.parse(contact.data)].map(input => [input.formSlug, input]), + ); + contact.data = Form.getAll(InputStatus.DEFAULT).map( + this.initField.bind(this), + ); + console.log(contact.data); + contact.data.map(input => { + const originalInput = originalData.get(input.formSlug); + // * Had this input before ? + if (originalInput !== undefined) { + // * Copy only if it is the same type + input.value = + input.type === originalInput.type ? originalInput.value : input.value; + } + }); + return contact; } + /** + * Get new instance of Contact + * @param {import('./../Models/Contact').ContactData} contact + * @returns {import('./../Models/Contact').ContactData} + */ + initNewContact(contact) { + // * Init DEFAULT Fields + contact.data = Form.getAll(InputStatus.DEFAULT).map( + this.initField.bind(this), + ); + contact.visits = []; + contact.notes = ''; + contact.app_user_id = this.props.currentUser.id; + return contact; + } + + /** + * Setup input field + * @param {import('./../Models/Form').Form} form + * @returns {import('./../Models/Contact').InputDataType} + */ + initField(form) { + const inputData = Object.assign({}, InputData); + inputData.title = form.placeholder[this.props.locale]; + inputData.required = form.required; + inputData.formSlug = form.slug; + inputData.type = form.type; + inputData.value = ''; + if ( + form.type === InputType.CHECKBOX || + form.type === InputType.SELECT_MULTIPLE + ) { + inputData.value = []; + } + return inputData; + } + + // * - VISITS METHODS + newVisit() { - this.state.visits.unshift({ title: this.state.newVisitTitle, note: '' }); - this.state.visits[this.state.selectedVisit].note = this.state.tmpVisitNote; + /** @type {typeof import('./../Models/Contact').ContactData} */ + const contact = this.state.contact; + contact.visits.unshift({ title: this.state.newVisitTitle, note: '' }); this.setState({ - tmpVisitNote: '', - visits: this.state.visits, + contact: contact, showNewVisit: false, - selectedVisit: 0, + selectedVisitIndex: 0, }); } @@ -218,18 +348,18 @@ class ContactDetailsScreen extends React.Component { { text: i18n.t('global.yes'), onPress: () => { - this.state.visits.splice(this.state.selectedVisit, 1); - if (this.state.visits.length === 0) { + /** @type {typeof import('./../Models/Contact').ContactData} */ + const contact = this.state.contact; + contact.visits.splice(this.state.selectedVisitIndex, 1); + if (contact.visits.length === 0) { this.setState({ - visits: this.state.visits, - selectedVisit: 0, - tmpVisitNote: '', + contact: contact, + selectedVisitIndex: 0, }); } else { this.setState({ - visits: this.state.visits, - selectedVisit: 0, - tmpVisitNote: this.state.visits[0].note, + contact: contact, + selectedVisitIndex: 0, }); } }, @@ -239,48 +369,68 @@ class ContactDetailsScreen extends React.Component { ); } - 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, - }); - if (contact.type === 'client') { - this.setState({ clientChecked: true }); - } else if (contact.type === 'prospect') { - this.setState({ prospectChecked: true }); - } else { - this.setState({ otherChecked: true }); + // * END - VISITS METHODS + + /** @type {import('expo-barcode-scanner').BarCodeScannedCallback} */ + handleBarCodeScanned({ type, data }) { + this.setState({ scanned: true }); + if (!data.includes('VCARD')) { + Alert.alert(i18n.t('contact_details_s.qrcode_not_vcard_format')); + return; } - 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 }); + /** @type {{ }} */ + let datas = vCard.parse(data); + /** @type {string[]} */ + const 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) { + const 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 }]; } - if (contact.products !== null) { - const tmp = JSON.parse(contact.products); - this.setState({ productsCheck: tmp }); - } + 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, + }); } + // * END --- METHODS + componentDidMount() { const { action } = this.props.route.params; - if (action === 'edit') { - this.setState({ action: 'edit' }); - this.populate(); - } + this.setState({ action: action === 'edit' ? 'edit' : 'add' }); BarCodeScanner.requestPermissionsAsync().then(res => { this.setState({ hasPermission: res.status === 'granted' }); @@ -288,229 +438,21 @@ class ContactDetailsScreen extends React.Component { } 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 ( + {/* LEFT SIDE -- FORMULARY VIEW */} - - 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 }); - } - }}> - - {this.renderSelect(1)} - - - - - { - if (itemValue !== '0') { - this.setState({ tmpSelect2: itemValue }); - } - }}> - - {this.renderSelect(2)} - - - - - {this.renderProductsCheck()} - + {/* ALL INPUTS : They are sorted on sync by server */} + {this.renderFormulary(Form.getAll()).map((input, inputIndex) => { + input.key = inputIndex; + return input; + })} + {/* BOTTOM SPACE */} + + {/* RIGHT SIDE -- VISITS AND NOTES */} + {/* VISITS AND NOTES */} this.setState({ selectedIndex: index })} + selectedIndex={this.state.selectedVisitsOrNoteTab} + onPress={index => + this.setState({ selectedVisitsOrNoteTab: index }) + } /> - {this.state.selectedIndex === 0 && ( + {/* VISITS */} + {this.state.selectedVisitsOrNoteTab === 0 && ( - {this.state.visits.length > 0 && ( + {this.state.contact.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, + selectedVisitIndex: itemIndex, }); }}> - {this.state.visits.map((item, index) => { + {this.state.contact.visits.map((item, index) => { return ( )} - {this.state.visits.length === 0 && ( + {this.state.contact.visits.length === 0 && ( this.setState({ showNewVisit: true })}> + onPress={() => { + const localDatetimeString = new Date().toLocaleString( + this.props.locale, + ); + this.setState({ + showNewVisit: true, + newVisitTitle: localDatetimeString.substring( + 0, + localDatetimeString.length - 3, + ), + }); + }}> - {this.state.visits.length > 0 && ( + {this.state.contact.visits.length > 0 && ( this.deleteVisit()}> @@ -603,49 +555,65 @@ class ContactDetailsScreen extends React.Component { )} - {this.state.visits.length > 0 && ( + {this.state.contact.visits.length > 0 && ( this.setState({ tmpVisitNote: text })} + value={ + this.state.contact.visits[this.state.selectedVisitIndex] + .note + } + onChangeText={text => { + /** @type {typeof import('./../Models/Contact').ContactData} */ + const contact = this.state.contact; + contact.visits[this.state.selectedVisitIndex].note = text; + this.setState({ contact: contact }); + }} /> )} )} - {this.state.selectedIndex === 1 && ( + {/* NOTES */} + {this.state.selectedVisitsOrNoteTab === 1 && ( this.setState({ tmpNote: text })} + value={this.state.contact.notes} + onChangeText={text => { + /** @type {typeof import('./../Models/Contact').ContactData} */ + const contact = this.state.contact; + contact.notes = text; + this.setState({ contact: contact }); + }} /> )} + {/* ACTIONS BUTTONS on BOTTOM */} {this.state.action === 'add' && ( - <> -