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
This commit is contained in:
Jérémy Munsch 2023-09-04 09:59:48 +02:00
parent 64c324d30a
commit 918e20a72b
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
41 changed files with 2929 additions and 1352 deletions

View file

@ -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.
- Ajout d'un espace plus important entre les éléments cliquables du header + ajout d'un menu en overlay.

153
ComApp/package-lock.json generated
View file

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

View file

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

View file

@ -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 = () => (

View file

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

View file

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

View file

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

View file

@ -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();

View file

@ -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<InputDataType>} */
data: [],
/** @type {string|null} */
notes: null,
/** @type {Array<Record<string,string>>|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<number,string>} */
value: '',
required: false,
};
/**
* @typedef {Unmanaged<typeof ContactData> & 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<typeof ContactData>} */
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<Contact>} */
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<Contact>}
*/
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<Contact>}
*/
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<Contact>}
*/
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;

View file

@ -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<typeof DirectoriesSchema.properties> & 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<Directory>} */
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<Directory>}
*/
getAll(role, authorizedDirs, format, lang) {
/** @type {Array<Directory>} */
const allowedDirs =
authorizedDirs !== null && authorizedDirs !== undefined
? JSON.parse(authorizedDirs)
: [];
/** @type {import('realm').Results<Directory>} */
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<Directory>} 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<string,string>} */
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<string,string>} name
* @returns {string}
*/
getI18nName(lang, name) {
/** @type {string} */
let tmp;
switch (lang) {
case 'fr':

View file

@ -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<typeof DocumentsSchema.properties> & 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<Document>} */
const tmp = this.realm.objects(this.schema);
if (tmp.length === 0) {
return null;
}
return tmp.filtered('id = ' + id)[0] ?? null;
}
/**
* @param {Array<import("./Directory").Directory>} authorizedDirs
* @returns {import('realm').Results<Document>}
*/
getAll(authorizedDirs = false) {
/** @type {Realm.Results<Document>} */
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<Document>} docs
* @param {Array<DocumentCleared>} toDelete
* @returns {Array<DocumentCleared>}
*/
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);
});

View file

@ -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<string>} list of translated placeholders Map<string,string> */
placeholder: {},
/** @type {import('realm').List<string>} list of json encoded translated details, list of Map<string,string> */
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<typeof FormData> & 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<Form>} */
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<Form>}
*/
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;

View file

@ -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<typeof SendBrochuresSchema.properties> & 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<SendBrochure>} */
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<SendBrochure>}
*/
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);

View file

@ -2,22 +2,36 @@ export const SettingsSchema = {
name: 'Settings',
primaryKey: 'id',
properties: {
/** @type {number} */
id: 'int',
key: 'string',
value: 'string',
},
};
/**
* @typedef {Unmanaged<typeof SettingsSchema.properties> & 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<Setting>} */
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);

View file

@ -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<typeof UsersSchema.properties> & 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<User>} */
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<User>}
*/
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());

View file

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

View file

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

View file

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

View file

@ -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<import('../../Models/Directory').Directory>, 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<import('../../Models/Directory').Directory>} directories Directories
* @param {import('../../Models/Document').Document} data The document data
*/
export const initDocuments = (lang, directories, data) => {
return {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<import('../../Models/Contact').Contact>}
*/
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: {

View file

@ -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<import('../../Models/Directory').Directory>} */
global: [],
/** @type {Realm.Results<import('../../Models/Directory').Directory>} */
treeview: [],
};
/**
* @param {typeof initialState} state
* @param {import('../Actions/directories').InitDirectoryAction} action
* @returns
*/
const reducer = (state = initialState, action) => {
switch (action.type) {
case INIT_DIRECTORIES: {

View file

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

View file

@ -0,0 +1,30 @@
import { INIT_FORMS } from '../Actions/actionType';
// Models
import { Form } from '../../../Database';
const initialState = {
/** @type {Realm.Results<import('../../Models/Form').Form>} */
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;

View file

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

View file

@ -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<import('../../Models/User').User>} */
users: [],
};
/**
* @param {typeof initialState} state
* @param {import('../Actions/users').InitUserAction} action
* @returns
*/
const reducer = (state = initialState, action) => {
switch (action.type) {
case INIT_USERS: {

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,18 @@ import {
Alert,
Button,
FlatList,
Linking,
Platform,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { openComposer } from 'react-native-email-link';
// Reduxs
import { connect } from 'react-redux';
import { deleteContact, editContact } from '../Store/Actions/contacts';
import { deleteContact, setContact } from '../Store/Actions/contacts';
// Helpers
import Helpers from '../Lib/Helpers';
// Translations
@ -36,9 +40,15 @@ function mapStateToProps(state) {
};
}
/**
* @typedef {(user: import('./../Models/User').User, data: import('./../Models/Contact').Contact) => void} setContact
* @typedef {(user: import('./../Models/User').User, uuid: string ) => void} deleteContact
* @typedef {(email: string ) => void} sendBrochure
*/
const mapDispatchToProps = dispatch => {
return {
editContact: (user, data) => dispatch(editContact(user, data)),
setContact: (user, data) => dispatch(setContact(user, data)),
deleteContact: (user, id) => dispatch(deleteContact(user, id)),
sendBrochure: email => dispatch({ type: 'SEND_BROCHURE', data: email }),
};
@ -50,9 +60,6 @@ class ContactScreen extends React.Component {
sortType: 'asc',
sortField: 'name',
searchTxt: '',
showTransferContact: false,
selectedTransferUser: 0,
contactToTransfer: {},
showSendBrochure: false,
selectedDocument: -1,
sendDocumentList: [],
@ -62,9 +69,21 @@ class ContactScreen extends React.Component {
tmpSendBrochureMessage: '',
sortedDocuments: [],
sortedEmails: [],
/** @type {number} The user index selected for transfert */
selectedTransferUserIndex: 0,
/** @type {import('./../Models/Contact').Contact|null} The contact to transfert */
contactToTransfer: null,
/** @type {boolean} Is tranfert contact model display ? */
showTransferContact: false,
};
// * --- ACTIONS
sendBrochure() {
// FIXME: Si on n'a pas consulté les documents la liste des documents est vide apres initialisation
if (
this.state.tmpSendBrochureEmail !== '' &&
this.state.tmpSendBrochureSubject !== '' &&
@ -72,14 +91,16 @@ class ContactScreen extends React.Component {
this.state.sendDocumentList.length > 0 &&
Helpers.isEmailValid(this.state.tmpSendBrochureEmail)
) {
/** @type {sendBrochure} */
const sendBrochure = this.props.sendBrochure;
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,
app_user_id: this.props.currentUser.id,
};
this.props.sendBrochure(email);
sendBrochure(email);
this.setState({
showSendBrochure: false,
tmpSendBrochureEmail: '',
@ -104,31 +125,25 @@ class ContactScreen extends React.Component {
}
transferContact() {
/** @type {import('./../Models/User').User} */
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.editContact(this.props.currentUser, contact);
this.setState({ showTransferContact: false });
)[this.state.selectedTransferUserIndex];
/** @type {import('./../Models/Contact').ContactData} cloned contact */
const transferContact = this.state.contactToTransfer.toJSON();
if (transferContact === null) {
throw new Error('No selected contact to transfert');
}
// * Assign cloned contact to another user
transferContact.send_to_app_user_id = nextUser.id;
transferContact.timestamp = new Date();
transferContact.synced = false;
/** @type {setContact} */
const setContactHook = this.props.setContact;
setContactHook(this.props.currentUser, transferContact);
this.setState({ showTransferContact: false, contactToTransfer: null });
showMessage({
message: i18n.t('contact_s.contact_transferred') + '.',
description: i18n.t('contact_s.contact_will_be_sent_after_sync') + '.',
@ -137,7 +152,11 @@ class ContactScreen extends React.Component {
});
}
deleteContactAction(id) {
/**
* Delete contact
* @param {import('../Models/Contact').Contact} contact
*/
deleteContactAction(contact) {
Alert.alert(
i18n.t('global.confirmation'),
i18n.t('contact_s.confirm_contact_delete'),
@ -151,7 +170,12 @@ class ContactScreen extends React.Component {
text: i18n.t('global.yes'),
onPress: () => {
this.setState({ contacts: [], sortedEmails: [] });
this.props.deleteContact(this.props.currentUser, id);
/** @type {setContact} */
const setContactHook = this.props.setContact;
/** @type {import('./../Models/Contact').ContactData} */
const contactToDelete = contact.toJSON();
contactToDelete.to_delete = true;
setContactHook(this.props.currentUser, contactToDelete);
showMessage({
message: i18n.t('contact_s.contact_deleted') + '.',
type: 'success',
@ -164,27 +188,112 @@ class ContactScreen extends React.Component {
);
}
Item(item) {
/**
* Try to call the number
*
* @param {string} phone
*/
callNumberIfSupported(phone) {
let phoneNumber = phone;
if (Platform.OS !== 'android') {
phoneNumber = `telprompt:${phone}`;
} else {
phoneNumber = `tel:${phone}`;
}
Linking.canOpenURL(phoneNumber)
.then(supported => {
if (supported) {
return Linking.openURL(phoneNumber);
}
})
.catch(err => console.log(err));
}
/**
* Write a new email to
*
* @param {string} email
*/
composeNewEmailTo(email) {
openComposer({
to: email,
});
}
// * END --- ACTIONS
/**
* Render the table header
* @returns {JSX.Element}
*/
HeaderItem() {
return (
<View style={styles.item}>
<TouchableOpacity
onPress={() => {
this.sortContacts('lastname');
}}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{i18n.t('global.lastname')} <FontAwesome5 name={'sort'} />
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
this.sortContacts('email');
}}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{i18n.t('global.email')} <FontAwesome5 name={'sort'} />
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
this.sortContacts('phone');
}}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{i18n.t('global.phone')} <FontAwesome5 name={'sort'} />
</Text>
</TouchableOpacity>
<Text style={styles.buttons} />
</View>
);
}
/**
* Render a line of contact
* @param {import('../Models/Contact').Contact} contact
* @returns
*/
Item(contact) {
const displayName =
String(contact.lastname ?? '').toUpperCase() + ' ' + contact.firstname;
return (
<View style={styles.item}>
<TouchableOpacity
onPress={() =>
this.props.navigation.navigate('ContactDetailsScreen', {
action: 'edit',
contactId: item.id,
contactId: contact.id,
})
}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{item.name.toUpperCase()} {item.firstname}
{contact.lastname && contact.firstname ? displayName : contact.id}
</Text>
</TouchableOpacity>
<Text style={styles.company}>{item.company}</Text>
<Text style={styles.email}>{item.email}</Text>
<TouchableOpacity onPress={() => this.composeNewEmailTo(contact.email)}>
<Text style={{ ...styles.clickable }}>{contact.email}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() =>
contact.phone ? this.callNumberIfSupported(contact.phone) : false
}>
<Text style={{ ...styles.clickable }}>{contact.phone}</Text>
</TouchableOpacity>
<View style={styles.buttons}>
<TouchableOpacity
style={styles.button}
onPress={() => {
this.deleteContactAction(item.id);
this.deleteContactAction(contact);
}}>
<FontAwesome5 name="trash" size={20} color={this.props.mainColor} />
</TouchableOpacity>
@ -193,7 +302,7 @@ class ContactScreen extends React.Component {
onPress={() => {
this.setState({
showTransferContact: true,
contactToTransfer: item,
contactToTransfer: contact,
});
}}>
<FontAwesome5
@ -225,31 +334,6 @@ class ContactScreen extends React.Component {
});
}
HeaderItem() {
return (
<View style={styles.item}>
<TouchableOpacity
onPress={() => {
this.sortContacts('name');
}}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{i18n.t('global.name')} <FontAwesome5 name={'sort'} />
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
this.sortContacts('company');
}}>
<Text style={{ ...styles.clickable, color: this.props.mainColor }}>
{i18n.t('global.company')} <FontAwesome5 name={'sort'} />
</Text>
</TouchableOpacity>
<Text style={styles.email}>{i18n.t('global.email')}</Text>
<Text style={styles.buttons} />
</View>
);
}
handleSearchBtn() {
if (this.state.searchTxt.length > 0) {
let obj = this.props.contacts;
@ -265,16 +349,34 @@ class ContactScreen extends React.Component {
}
handleSearchTxt(text) {
this.setState({ searchTxt: text });
/** @type {Array<import('./../Models/Contact').Contact>} */
const contacts = this.props.contacts;
if (text.length > 0) {
let tmpContactsName = this.props.contacts.filter(
item => item.name.toLowerCase().indexOf(text.toLowerCase()) >= 0,
const tmpContactsLastname = contacts.filter(
item => item.lastname?.toLowerCase()?.indexOf(text.toLowerCase()) >= 0,
);
let tmpContactsCompany = this.props.contacts.filter(
item => item.company.toLowerCase().indexOf(text.toLowerCase()) >= 0,
const tmpContactsFirstname = contacts.filter(
item => item.firstname?.toLowerCase()?.indexOf(text.toLowerCase()) >= 0,
);
let resSearch = Object.assign(tmpContactsName, tmpContactsCompany);
const tmpContactsEmail = contacts.filter(
item => item.email.toLowerCase().indexOf(text.toLowerCase()) >= 0,
);
const tmpContactsPhone = contacts.filter(
item => item.phone?.toLowerCase()?.indexOf(text.toLowerCase()) >= 0,
);
const resSearch = new Map();
tmpContactsLastname.forEach(contact =>
resSearch.set(contact.id, contact),
);
tmpContactsFirstname.forEach(contact =>
resSearch.set(contact.id, contact),
);
tmpContactsEmail.forEach(contact => resSearch.set(contact.id, contact));
tmpContactsPhone.forEach(contact => resSearch.set(contact.id, contact));
this.setState({
contacts: resSearch.sort(Helpers.compareValues('name', 'asc')),
contacts: [...resSearch.values()].sort(
Helpers.compareValues('name', 'asc'),
),
sortType: 'asc',
});
} else {
@ -320,7 +422,7 @@ class ContactScreen extends React.Component {
}
this.setState({
contacts: arrayContacts.sort(Helpers.compareValues('name', 'asc')),
contacts: arrayContacts.sort(Helpers.compareValues('timestamp', 'desc')),
sortedDocuments: arrayDocs.sort(Helpers.compareValues('title', 'asc')),
sortedEmails: arrayEmails.sort(Helpers.compareValues('email', 'asc')),
sortType: 'asc',
@ -341,6 +443,7 @@ class ContactScreen extends React.Component {
}
return (
<View style={styles.container}>
{/* ACTIONS */}
<View
style={{
marginTop: 20,
@ -383,6 +486,7 @@ class ContactScreen extends React.Component {
</TouchableOpacity>
</View>
</View>
{/* TABLE view */}
<View style={{ flex: 1, marginTop: 20 }}>
<FlatList
data={this.state.contacts}
@ -391,6 +495,7 @@ class ContactScreen extends React.Component {
keyExtractor={item => item.id.toString()}
/>
</View>
{/* TRANSFERT CONTACT POPUP */}
<Overlay
isVisible={this.state.showTransferContact}
windowBackgroundColor="rgba(255, 255, 255, .9)"
@ -408,12 +513,13 @@ class ContactScreen extends React.Component {
marginBottom: 15,
paddingLeft: 10,
}}
selectedValue={this.state.selectedTransferUser}
selectedValue={this.state.selectedTransferUserIndex}
onValueChange={(itemValue, itemIndex) => {
this.setState({ selectedTransferUser: itemIndex });
this.setState({ selectedTransferUserIndex: itemIndex });
}}>
{this.props.users
.filter(user => user.id !== this.props.currentUser.id)
.sort(Helpers.compareValues('email', 'asc'))
.map((item, index) => {
return (
<Picker.Item
@ -444,6 +550,7 @@ class ContactScreen extends React.Component {
</View>
</View>
</Overlay>
{/* DELETE CONTACT POPUP */}
<Overlay
isVisible={this.state.showSendBrochure}
windowBackgroundColor="rgba(255, 255, 255, .9)"

View file

@ -9,10 +9,10 @@ import {
// Common Styles
import commonStyles from '../../Resources/CommonStyles';
// Redux
import {connect} from 'react-redux';
import {initContact} from '../Store/Actions/contacts';
import {initDirectories} from '../Store/Actions/directories';
import {initDocuments} from '../Store/Actions/documents';
import { connect } from 'react-redux';
import { initContact } from '../Store/Actions/contacts';
import { initDirectories } from '../Store/Actions/directories';
import { initDocuments } from '../Store/Actions/documents';
// SplashScreen
import * as SplashScreen from 'expo-splash-screen';
// Style
@ -37,7 +37,7 @@ function mapStateToProps(state) {
const mapDispatchToProps = dispatch => {
return {
initBtns: () => dispatch({type: 'INIT_HOME_BTNS'}),
initBtns: () => dispatch({ type: 'INIT_HOME_BTNS' }),
initDocuments: (lang, directories) =>
dispatch(initDocuments(lang, directories)),
initContact: currentUser => dispatch(initContact(currentUser)),
@ -55,6 +55,7 @@ class HomeScreen extends React.Component {
return this.openSync();
}
this.props.initBtns();
console.debug(this.props.currentUser);
this.props.initContact(this.props.currentUser);
this.props.initDirectories(this.props.locale, this.props.currentUser);
this.props.initDocuments(this.props.locale, this.props.directories.global);
@ -68,7 +69,7 @@ class HomeScreen extends React.Component {
let seconds =
Math.abs(new Date(this.state.lastSyncCall) - Date.now()) / 1000;
if (seconds > 30) {
this.setState({lastSyncCall: Date.now()});
this.setState({ lastSyncCall: Date.now() });
SyncController.needSync(this.openSync.bind(this));
}
}
@ -76,14 +77,14 @@ class HomeScreen extends React.Component {
}
openSync() {
this.props.navigation.navigate('SyncScreen', {forceSync: true});
this.props.navigation.navigate('SyncScreen', { forceSync: true });
}
render() {
i18n.locale = this.props.locale;
return (
<ImageBackground
source={{uri: FileSystem.documentDirectory + 'BackgroundImage.png'}}
source={{ uri: FileSystem.documentDirectory + 'BackgroundImage.png' }}
style={commonStyles.background}
resizeMode="cover">
<View style={styles.container}>
@ -138,7 +139,9 @@ class HomeScreen extends React.Component {
onPress={() => this.openFolder(item.id)}
style={styles.button}>
<Image
source={{uri: FileSystem.documentDirectory + 'HomeButtonImage.png'}}
source={{
uri: FileSystem.documentDirectory + 'HomeButtonImage.png',
}}
style={styles.btnImg}
/>
<Text style={styles.btnTxt}>{name}</Text>
@ -150,7 +153,7 @@ class HomeScreen extends React.Component {
openFolder(id) {
this.props.initDocuments(this.props.locale, this.props.directories.global);
const folder = this.props.directories.treeview.filter(tmp => tmp.id === id);
this.props.navigation.navigate('DirectoriesScreen', {folder: folder});
this.props.navigation.navigate('DirectoriesScreen', { folder: folder });
}
}

View file

@ -139,9 +139,6 @@ class InstallScreen extends React.Component {
switch (key) {
case 'MainColor':
case 'AppStoreUUID':
case 'FormSelect1':
case 'FormSelect2':
case 'FormCheck':
Setting.upsert({ key: key, value: value });
break;
case 'SquaredLogo':

View file

@ -107,17 +107,17 @@ class LoginScreen extends React.Component {
);
return;
}
var usersSize = Object.keys(this.props.users).length;
if (usersSize === 0) {
/** @type {Array<import('../Models/User').User>} */
const users = this.props.users;
if (users.length === 0) {
Alert.alert(i18n.t('global.warning'), i18n.t('login_s.missing_users'));
return;
}
let user = this.props.users.filter(tmp => tmp.email === login);
if (user.length === 0) {
const user = users.find(tmp => tmp.email === login);
if (user === undefined) {
Alert.alert(i18n.t('error.error'), i18n.t('error.incorrect_credentials'));
return;
}
user = user[0];
const verified = LoginController.verify(user, password).then(
verified => verified,
);
@ -135,9 +135,11 @@ class LoginScreen extends React.Component {
return false;
});
const lastUser = Setting.get('lastUser');
// Si on est sur une installation
if (lastUser === false || lastUser == undefined) {
// Si la tablette est connectée à internet pour forcer la synchro
// * Si on est sur une installation
if (lastUser === null) {
console.debug('New Install');
// * Si la tablette est connectée à internet pour forcer la synchro
if (!isConnected) {
Alert.alert(
i18n.t('error.error'),
@ -147,33 +149,35 @@ class LoginScreen extends React.Component {
}
return this.loginAction(user);
}
// Si on a deja une install
if (lastUser !== false && lastUser !== undefined) {
// Si on a une précédente connexion
if (lastUser.value === user.id.toString()) {
this.loginAction(user);
} else {
// Si le user precedent est different du user courant
// Si user !== last user && last user not synced
if (!SyncController.isSynced()) {
Alert.alert(
i18n.t('error.error'),
i18n.t('error.tablet_not_synced_last_user'),
);
return;
}
// Si la tablette est connectée à internet pour forcer la synchro
if (!isConnected) {
Alert.alert(
i18n.t('error.error'),
i18n.t('error.tablet_not_connected_internet'),
);
return;
}
SyncController.setInstalled(true);
this.loginAction(user, true);
}
// * On a deja eu une install
// * Si on a une précédente connexion
if (lastUser.value === user.id.toString()) {
console.debug('Was previously logged in');
return this.loginAction(user);
}
// Si le user precedent est different du user courant
// Si user !== last user && last user not synced
if (!SyncController.isSynced(user.id)) {
Alert.alert(
i18n.t('error.error'),
i18n.t('error.tablet_not_synced_last_user'),
);
return;
}
// Si la tablette est connectée à internet pour forcer la synchro
if (!isConnected) {
Alert.alert(
i18n.t('error.error'),
i18n.t('error.tablet_not_connected_internet'),
);
return;
}
console.debug('New user loggin');
SyncController.setInstalled(true);
return this.loginAction(user, true);
}
async syncUsersIfPossibleAsync() {
@ -205,8 +209,12 @@ class LoginScreen extends React.Component {
}
}
/**
* @param {import('../Models/User').User} user
*/
loginAction(user) {
this.setState({ login: '', password: '' });
console.debug('loginAction');
LoginController.login(user);
this.props.setCurrentUser();
}

View file

@ -20,6 +20,7 @@ import {
Contact,
Directory,
Document,
Form,
SendBrochure,
User,
} from '../../Database';
@ -40,6 +41,7 @@ const mapDispatchToProps = dispatch => {
return {
init: () => dispatch({ type: 'INIT' }),
initUsers: () => dispatch({ type: 'INIT_USERS' }),
initFormularies: () => dispatch({ type: 'INIT_FORMS' }),
initDirectories: (lang, user) => dispatch(initDirectories(lang, user)),
initDocuments: (lang, directories) =>
dispatch(initDocuments(lang, directories)),
@ -106,15 +108,48 @@ class SyncScreen extends React.Component {
async sync() {
let errorStatus = false;
const isConnected = await SyncController.checkConnection()
.then(state => {
//console.log(state.isConnected)
return state.isConnected;
})
.catch(error => {
//console.log(error);
return false;
});
console.debug('Sync');
/**
* @param {Promise<import('@react-native-community/netinfo').NetInfoState>} promise
* @param {(error:any) => void|null} onError
*/
const getCall = (promise, onError = null) => {
if (onError === null) {
/** @param {import('axios').AxiosError} error */
onError = error => {
let message = 'Erreur de la communication serveur';
if (error?.response?.status === 422) {
message =
this.parseValidationErrors(error.response?.data?.errors ?? {}) ||
message;
console.warn(error.response?.data?.errors ?? {});
} else if (error?.response?.status === 419) {
// * CSRF TOKEN MISMATCH (On ne pourrais plus faire d'appels AJAX sans recharger la page)
message = 'Votre session a expiré, la page va être rechargée';
} else if (error?.response?.status === 403) {
// * Access denied
message = "Vous n'êtes pas autorisé a effectuer cette action";
} else {
console.error(error);
}
console.warn(message);
Sentry.captureException(error);
Alert.alert(
i18n.t('error.error'),
i18n.t('error.error_has_occurred'),
);
errorStatus = true;
};
}
return promise.catch(onError);
};
const isConnected = await SyncController.checkConnection().catch(
error => false,
);
if (!isConnected) {
Alert.alert(
i18n.t('global.warning'),
@ -123,121 +158,150 @@ class SyncScreen extends React.Component {
return;
}
this.setState({ processing: true });
// getVersion
const resVersion = await SyncController.getVersion()
.then(res => {
// * --- getVersion
const resVersion = await getCall(
SyncController.getVersion().then(res => {
return res.data;
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(i18n.t('error.error'), i18n.t('error.error_has_occurred'));
errorStatus = true;
});
}),
);
if (errorStatus) {
return false;
}
this.props.updateVersion(resVersion.version);
this.props.updateShowIsActive(resVersion.ShowIsActive);
this.setState({ newApp: resVersion.newApp });
// getUsers
this.setState({ txtStep: i18n.t('sync_s.updating_users') + '...' });
const resUsers = await SyncController.getUsers()
.then(res => {
// * --- getUsers
this.setState({
progress: 0.1,
txtStep: i18n.t('sync_s.updating_users') + '...',
});
const resUsers = await getCall(
SyncController.getUsers().then(res => {
User.clear();
return res.data;
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(i18n.t('error.error'), i18n.t('error.error_has_occurred'));
errorStatus = true;
});
}),
);
if (errorStatus) {
return false;
}
resUsers.forEach(user => {
User.add(user);
});
// getContacts
this.setState({
progress: 0.1,
txtStep: i18n.t('sync_s.updating_contacts') + '...',
});
const resContacts = await SyncController.getContacts(
this.props.currentUser.id,
)
.then(res => {
Contact.clear();
return res.data;
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(i18n.t('error.error'), i18n.t('error.error_has_occurred'));
errorStatus = true;
});
if (errorStatus) {
return false;
}
resContacts.forEach(contact => {
Contact.add(contact);
});
//sendBrochures
// * --- Contacts Sync
/*
* This is done only for actual app user
- 1 Upsert all contacts
- 2 Delete contacts where to_delete is true
- 3 Get all contacts
*/
this.setState({
progress: 0.2,
txtStep: i18n.t('sync_s.updating_contacts') + '...',
});
// * --- Contacts Sync -- STEP 1
await getCall(SyncController.upsertContacts(this.props.currentUser.id));
if (errorStatus) {
return false;
}
// * --- Contacts Sync -- STEP 2
this.setState({
progress: 0.23,
});
await getCall(SyncController.deleteContacts(this.props.currentUser.id));
if (errorStatus) {
return false;
}
// * --- Contacts Sync -- STEP 3
this.setState({
progress: 0.26,
});
const resContacts = await getCall(
SyncController.getContacts(this.props.currentUser.id).then(res => {
// * Clear Contacts, in order to fetch them all after.
Contact.clear(this.props.currentUser.id);
return res.data;
}),
);
if (errorStatus) {
return false;
}
/** @type {(contact: import('./../Models/Contact').ContactData ) => void} */
const contactMapper = contact => {
// * Stringify complex Fields
contact.data = JSON.stringify(contact.data);
contact.visits = JSON.stringify(contact.visits);
contact.timestamp = new Date(contact.timestamp);
Contact.set(contact);
};
resContacts.forEach(contactMapper);
// * --- sendBrochures
this.setState({
progress: 0.3,
txtStep: i18n.t('sync_s.sending_flyers') + '...',
});
let brochures = SendBrochure.getAll();
const brochures = SendBrochure.getAll();
for (const brochure of brochures) {
await SyncController.sendBrochure(brochure)
.then(res => {
await getCall(
SyncController.sendBrochure(brochure).then(res => {
SendBrochure.delete(brochure.id);
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(
i18n.t('error.error'),
i18n.t('error.error_has_occurred') + ' : Brochures',
);
errorStatus = true;
});
}),
);
}
if (errorStatus) {
return false;
}
//getDirectories
// * --- getDirectories
this.setState({
progress: 0.3,
progress: 0.4,
txtStep: i18n.t('sync_s.updating_folders') + '...',
});
const resDirectories = await SyncController.getDirectories()
.then(res => {
const resDirectories = await getCall(
SyncController.getDirectories().then(res => {
Directory.clear();
return res.data;
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(i18n.t('error.error'), i18n.t('error.error_has_occurred'));
errorStatus = true;
});
}),
);
if (errorStatus) {
return false;
}
resDirectories.forEach(dir => {
Directory.add(dir);
});
// getDocuments
// * --- getFormularies
this.setState({
progress: 0.4,
progress: 0.5,
txtStep: i18n.t('sync_s.updating_forms') + '...',
});
const resFormularies = await getCall(
SyncController.getFormularies().then(res => {
Form.clear();
return res.data;
}),
);
if (errorStatus) {
return false;
}
resFormularies.forEach(form => Form.add(form));
// * --- getDocuments
this.setState({
progress: 0.6,
txtStep: i18n.t('sync_s.updating_docs') + '...',
});
const resDocuments = await SyncController.getDocuments()
.then(res => {
const resDocuments = await getCall(
SyncController.getDocuments().then(res => {
return res.data;
})
.catch(error => {
Sentry.captureException(error);
Alert.alert(i18n.t('error.error'), i18n.t('error.error_has_occurred'));
errorStatus = true;
});
}),
);
if (errorStatus) {
return false;
}
@ -246,11 +310,8 @@ class SyncScreen extends React.Component {
Document.update(document);
});
this.downloadDocuments(Document.getAll(), toDelete);
// End
this.setState({
txtStep: i18n.t('sync_s.update_completed') + '.',
processing: false,
});
// * --- End
}
fileProgress() {
@ -263,11 +324,11 @@ class SyncScreen extends React.Component {
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.setState({ progress: 0.6 });
const docProgress = 0.4 / toDownload.length;
this.download(toDownload, docProgress, 0).then(status => {
if (status === true) {
this.deleteFile(toDelete, 0).then(status => {
this.deleteFile(toDelete, 0).then(delStatus => {
this.finishSync(version);
});
} else {
@ -418,6 +479,27 @@ class SyncScreen extends React.Component {
);
}
}
/**
* Parse error messages for a specific input.
*
* @param {Record<string, Array<string>>} allErrors
* @return {string | null}
*/
parseValidationErrors(allErrors = {}) {
const errorList = Object.values(allErrors);
if (!errorList.length) {
return null;
}
return errorList
.reduce((oldInputErrors, inputErrors) =>
oldInputErrors ? oldInputErrors.concat(inputErrors) : inputErrors,
)
.reduce(
(oldInputString, inputString) =>
(oldInputString ? `${oldInputString}, ` : '') + inputString,
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SyncScreen);

View file

@ -3,6 +3,7 @@ import Realm from 'realm';
import ContactModel, { ContactsSchema } from './App/Models/Contact';
import DirectoryModel, { DirectoriesSchema } from './App/Models/Directory';
import DocumentModel, { DocumentsSchema } from './App/Models/Document';
import FormModel, { FormsSchema } from './App/Models/Form';
import SendBrochureModel, {
SendBrochuresSchema,
} from './App/Models/SendBrochure';
@ -17,15 +18,17 @@ const Database = {
SendBrochuresSchema,
SettingsSchema,
UsersSchema,
FormsSchema,
],
//encryptionKey: new Int8Array(64)
};
let realm = new Realm(Database);
export let Contact = new ContactModel(realm);
export let Directory = new DirectoryModel(realm);
export let Document = new DocumentModel(realm);
export let SendBrochure = new SendBrochureModel(realm);
export let Setting = new SettingModel(realm);
export let User = new UserModel(realm);
export const Contact = new ContactModel(realm);
export const Directory = new DirectoryModel(realm);
export const Document = new DocumentModel(realm);
export const SendBrochure = new SendBrochureModel(realm);
export const Setting = new SettingModel(realm);
export const User = new UserModel(realm);
export const Form = new FormModel(realm);

View file

@ -1,118 +1,120 @@
{
"install": {
"installation": "Installation",
"install": "Installieren Sie",
"enter_url": "Bitte geben Sie den Link zu Ihrem Backoffice ein",
"url_invalid": "Der eingegebene Link ist ungültig.",
"resource_dl_error": "Beim Herunterladen der folgenden Lerneinheit ist ein Fehler aufgetreten: ",
"install_complete": "Installation abgeschlossen!",
"get_app_setting": "Wiederherstellen der Konfiguration der Anwendung",
"get_content_setting": "Abrufen der Konfiguration des Inhalts der Anwendung"
},
"global": {
"change_lang": "Sprache wechseln",
"sync": "Synchronisation",
"logout": "Abmeldung",
"close": "Schließen Sie",
"install": "Installieren",
"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": {
"username": "Benutzername",
"password": "Passwort"
},
"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"
}
}
{
"install": {
"installation": "Installation",
"install": "Installieren Sie",
"enter_url": "Bitte geben Sie den Link zu Ihrem Backoffice ein",
"url_invalid": "Der eingegebene Link ist ungültig.",
"resource_dl_error": "Beim Herunterladen der folgenden Lerneinheit ist ein Fehler aufgetreten: ",
"install_complete": "Installation abgeschlossen!",
"get_app_setting": "Wiederherstellen der Konfiguration der Anwendung",
"get_content_setting": "Abrufen der Konfiguration des Inhalts der Anwendung"
},
"global": {
"change_lang": "Sprache wechseln",
"sync": "Synchronisation",
"logout": "Abmeldung",
"close": "Schließen Sie",
"install": "Installieren",
"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",
"lastname": "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": {
"username": "Benutzername",
"password": "Passwort"
},
"search_s": {
"no_result": "Keine Ergebnisse"
},
"sync_s": {
"download": "Download",
"update_completed": "Update abgeschlossen",
"updating_docs": "Dokumentaktualisierung",
"updating_forms": "Formulare aktualisieren",
"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 einen Benutzer aus",
"transfer": "weiterleiten",
"contact_list": "Kontaktliste",
"documents_list": "Liste der Dokumente",
"input_required": "Das Feld %{placeholder} ist erforderlich",
"input_email": "Das Feld %{placeholder} muss eine gültige E-Mail-Adresse sein"
},
"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",
"description": "Beschreibung",
"no_visits": "Kein Besuch",
"visit_name": "Besuchsname",
"input_select_please_choose": "Wähle einen Wert",
"input_select_multiple_search": "Suchen"
},
"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",
"invalid_firstname": "Vorname ist ungültig",
"invalid_lastname": "Nachname ist ungültig",
"invalid_phone": "Die Telefonnummer ist ungültig",
"field_required": "Das Feld %{placeholder} ist erforderlich",
"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.",
"camera_permission_denied": "Kameraerlaubnis verweigert",
"pending_camera_permission": "Warten auf Kameraerlaubnis"
}
}

View file

@ -1,118 +1,120 @@
{
"install": {
"installation": "Installation",
"install": "Install",
"enter_url": "Please enter your back office link",
"url_invalid": "The link you entered is invalid.",
"resource_dl_error": "An error occurred while downloading the following resource: ",
"install_complete": "Installation is complete!",
"get_app_setting": "Retrieving the application's configuration",
"get_content_setting": "Retrieving the configuration of the application content"
},
"global": {
"change_lang": "Change language",
"sync": "Synchronization",
"logout": "Logout",
"close": "Close",
"install": "Install",
"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": {
"username": "Username",
"password": "Password"
},
"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"
}
}
{
"install": {
"installation": "Installation",
"install": "Install",
"enter_url": "Please enter your back office link",
"url_invalid": "The link you entered is invalid.",
"resource_dl_error": "An error occurred while downloading the following resource: ",
"install_complete": "Installation is complete!",
"get_app_setting": "Retrieving the application's configuration",
"get_content_setting": "Retrieving the configuration of the application content"
},
"global": {
"change_lang": "Change language",
"sync": "Synchronization",
"logout": "Logout",
"close": "Close",
"install": "Install",
"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",
"lastname": "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": {
"username": "Username",
"password": "Password"
},
"search_s": {
"no_result": "No Result"
},
"sync_s": {
"download": "Download",
"update_completed": "Update Done",
"updating_docs": "Updating documents",
"updating_forms": "Updating forms",
"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 a user",
"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",
"description": "Description",
"no_visits": "No visits available",
"visit_name": "visits name",
"input_select_please_choose": "Select a value",
"input_select_multiple_search": "Search",
"input_required": "The %{placeholder} field is required",
"input_email": "The %{placeholder} field must be a valid email"
},
"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",
"invalid_firstname": "Firstname is not valid",
"invalid_lastname": "Lastname is not valid",
"invalid_phone": "Phone is not valid",
"field_required": "The %{placeholder} field is required",
"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",
"camera_permission_denied": "Camera Permission Denied",
"pending_camera_permission": "Waiting for the camera permission"
}
}

View file

@ -1,122 +1,124 @@
{
"install": {
"installation": "Installation",
"install": "Installer",
"enter_url": "Veuillez entrer le lien de votre back-office",
"url_invalid": "Le lien saisi est invalide.",
"resource_dl_error": "Une erreur est survenue lors du téléchargement de la ressource suivante : ",
"install_complete": "Installation terminée !",
"get_app_setting": "Récupération de la configuration de l'application",
"get_content_setting": "Récupération de la configuration du contenu de l'application"
},
"global": {
"change_lang": "Changer de langue",
"sync": "Synchronisation",
"logout": "Se déconnecter",
"close": "Fermer",
"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": {
"username": "Identifiant",
"password": "Mot de passe",
"missing_users": "Utilisateurs introuvables"
},
"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"
},
"alert": {
"error": "Erreur",
"info": "Information"
}
}
{
"install": {
"installation": "Installation",
"install": "Installer",
"enter_url": "Veuillez entrer le lien de votre back-office",
"url_invalid": "Le lien saisi est invalide.",
"resource_dl_error": "Une erreur est survenue lors du téléchargement de la ressource suivante : ",
"install_complete": "Installation terminée !",
"get_app_setting": "Récupération de la configuration de l'application",
"get_content_setting": "Récupération de la configuration du contenu de l'application"
},
"global": {
"change_lang": "Changer de langue",
"sync": "Synchronisation",
"logout": "Se déconnecter",
"close": "Fermer",
"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",
"lastname": "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": {
"username": "Identifiant",
"password": "Mot de passe",
"missing_users": "Utilisateurs introuvables"
},
"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_forms": "Mise à jour des formulaires",
"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 un utilisateur",
"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",
"description": "Description",
"no_visits": "Aucune visites",
"visit_name": "Nom de la visite",
"input_select_please_choose": "Selectionnez un élément",
"input_select_multiple_search": "Recherche",
"input_required": "Le champ %{placeholder} est obligatoire",
"input_email": "Le champ %{placeholder} doit être un email valide"
},
"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",
"invalid_firstname": "Prénom invalide",
"invalid_lastname": "Nom invalide",
"invalid_phone": "Téléphone invalide",
"field_required": "Le champ %{placeholder} est obligatoire",
"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",
"camera_permission_denied": "Permission caméra refusée",
"pending_camera_permission": "En attente de permission caméra"
},
"alert": {
"error": "Erreur",
"info": "Information"
}
}

View file

@ -3378,7 +3378,7 @@
"resolved" "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
"version" "1.1.2"
"buffer@^5.5.0", "buffer@^5.6.0":
"buffer@^5.5.0":
"integrity" "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="
"resolved" "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
"version" "5.7.1"
@ -3386,6 +3386,22 @@
"base64-js" "^1.3.1"
"ieee754" "^1.1.13"
"buffer@^5.6.0":
"integrity" "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="
"resolved" "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
"version" "5.7.1"
dependencies:
"base64-js" "^1.3.1"
"ieee754" "^1.1.13"
"buffer@^6.0.3":
"integrity" "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="
"resolved" "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
"version" "6.0.3"
dependencies:
"base64-js" "^1.3.1"
"ieee754" "^1.2.1"
"builtins@^1.0.3":
"integrity" "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ=="
"resolved" "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz"
@ -5874,7 +5890,7 @@
dependencies:
"safer-buffer" ">= 2.1.2 < 3.0.0"
"ieee754@^1.1.13":
"ieee754@^1.1.13", "ieee754@^1.2.1":
"integrity" "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
"resolved" "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
"version" "1.2.1"
@ -7317,6 +7333,11 @@
"resolved" "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
"version" "4.0.8"
"lodash.isequal@^4.5.0":
"integrity" "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
"resolved" "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
"version" "4.5.0"
"lodash.merge@^4.6.2":
"integrity" "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
"resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
@ -7477,6 +7498,11 @@
"resolved" "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
"version" "5.2.1"
"memoize-one@^6.0.0":
"integrity" "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
"resolved" "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz"
"version" "6.0.0"
"memory-cache@~0.2.0":
"integrity" "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA=="
"resolved" "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz"
@ -9029,7 +9055,7 @@
dependencies:
"read" "1"
"prop-types@*", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1":
"prop-types@*", "prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1":
"integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="
"resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
"version" "15.8.1"
@ -9199,6 +9225,11 @@
"jscodeshift" "^0.13.1"
"nullthrows" "^1.1.1"
"react-native-email-link@^1.14.6":
"integrity" "sha512-XsWgdSMgxutKrm6WJRLKd1DxQpFt2x6xLuIhmAE5CdMMjNKadsUM5Evub9+otTBhlphSerePAwzSaNoBXpisLg=="
"resolved" "https://registry.npmjs.org/react-native-email-link/-/react-native-email-link-1.14.6.tgz"
"version" "1.14.6"
"react-native-flash-message@^0.3.1":
"integrity" "sha512-PswNvDdHBOavmIM7G4loLuEwML3oB8E8wVCuOPQ0Q6hRbExwTaF6gsTJvo5fQkyUsCi27ainSkL/F0ZijIkVig=="
"resolved" "https://registry.npmjs.org/react-native-flash-message/-/react-native-flash-message-0.3.1.tgz"
@ -9263,6 +9294,14 @@
"react-freeze" "^1.0.0"
"warn-once" "^0.1.0"
"react-native-sectioned-multi-select@^0.10.0":
"integrity" "sha512-wD5USgM88cHNkgQLB94NxbubtYCLacgzk2vfFlF+HvOI7noDz1AZfzu2sVp1WKz4bFx20MFxM4mi1uZoyxAutg=="
"resolved" "https://registry.npmjs.org/react-native-sectioned-multi-select/-/react-native-sectioned-multi-select-0.10.0.tgz"
"version" "0.10.0"
dependencies:
"lodash.isequal" "^4.5.0"
"memoize-one" "^6.0.0"
"react-native-size-matters@^0.4.0":
"integrity" "sha512-8/C0htHrFWeUm9N8JegmadovUfgTWkGBkDPZ1N3YkXtDWb+98Ya2gThiKcu445r8c7YhcrBfnHz/mYsXIusaOQ=="
"resolved" "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.4.0.tgz"
@ -9276,6 +9315,11 @@
"css-select" "^5.1.0"
"css-tree" "^1.1.3"
"react-native-uuid@^2.0.1":
"integrity" "sha512-cptnoIbL53GTCrWlb/+jrDC6tvb7ypIyzbXNJcpR3Vab0mkeaaVd5qnB3f0whXYzS+SMoSQLcUUB0gEWqkPC0g=="
"resolved" "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.1.tgz"
"version" "2.0.1"
"react-native-vector-icons@^9.2.0", "react-native-vector-icons@>7.0.0":
"integrity" "sha512-wKYLaFuQST/chH3AJRjmOLoLy3JEs1JR6zMNgTaemFpNoXs0ztRnTxcxFD9xhX7cJe1/zoN5BpQYe7kL0m5yyA=="
"resolved" "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz"
@ -9284,7 +9328,7 @@
"prop-types" "^15.7.2"
"yargs" "^16.1.1"
"react-native@*", "react-native@^0.70.6", "react-native@>=0.42.0", "react-native@>=0.56.0", "react-native@>=0.57", "react-native@>=0.59", "react-native@>=0.70.0":
"react-native@*", "react-native@^0.70.6", "react-native@>=0.40.0", "react-native@>=0.42.0", "react-native@>=0.56.0", "react-native@>=0.57", "react-native@>=0.59", "react-native@>=0.70.0":
"integrity" "sha512-xtQdImPHnwgraEx3HIZFOF+D1hJ9bC5mfpIdUGoMHRws6OmvHAjmFpO6qfdnaQ29vwbmZRq7yf14sbury74R/w=="
"resolved" "https://registry.npmjs.org/react-native/-/react-native-0.70.6.tgz"
"version" "0.70.6"
@ -9356,7 +9400,7 @@
"react-shallow-renderer" "^16.15.0"
"scheduler" "^0.22.0"
"react@*", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^18.1.0", "react@>=16", "react@>=16.4.1", "react@>=17.0.0", "react@15.x || 16.x || 17.x || 18.x", "react@18.1.0":
"react@*", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^18.1.0", "react@>=16", "react@>=16.4.1", "react@>=16.8.0", "react@>=17.0.0", "react@15.x || 16.x || 17.x || 18.x", "react@18.1.0":
"integrity" "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ=="
"resolved" "https://registry.npmjs.org/react/-/react-18.1.0.tgz"
"version" "18.1.0"