ldc-com-app/ComApp/src/App/Views/ContactDetailsScreen.js

1615 lines
52 KiB
JavaScript

import React from 'react';
import {
Alert,
Button,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
// Reduxs
import { connect } from 'react-redux';
import { setContact } from '../Store/Actions/contacts';
// Helpers
import Helpers from '../Lib/Helpers';
// Translations
import i18n from '../../I18n';
// Icons
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
// Style
import styles from '../../Resources/Styles/ContactDetailsStyle';
// UI Elements
import { ButtonGroup, CheckBox, Overlay } from '@rneui/themed';
// Flash Messages
import { showMessage } from 'react-native-flash-message';
// Picker
import { Picker } from '@react-native-picker/picker';
// * Select List
import SectionedMultiSelect from 'react-native-sectioned-multi-select';
// vCard parser
import vCard from 'vcard-parser';
// BarCode Scanner
import { BarCodeScanner } from 'expo-barcode-scanner';
// FormModel
import { InputStatus } from '../Enums/InputStatus';
import { InputType } from '../Enums/InputType';
import { Contact, Form } from './../../Database';
import { InputData } from './../Models/Contact';
function mapStateToProps(state) {
return {
locale: state.locale.locale,
contacts: state.contacts,
currentUser: state.app.currentUser,
mainColor: state.app.mainColor,
};
}
/**
* @typedef {(user: import('./../Models/User').User, data: import('./../Models/Contact').Contact) => any} setContact
*/
const mapDispatchToProps = dispatch => {
return {
setContact: (user, data) => dispatch(setContact(user, data)),
};
};
class ContactDetailsScreen extends React.Component {
constructor(props) {
super(props);
// * This is to hold array of number for multiple inputs selects
const multipleSelectInputs = Object.fromEntries(
new Map(
Form.getAll()
.filter(form => form.type === InputType.SELECT_MULTIPLE)
.map(form => [`input${form.id}`, { show: false, selectedItems: [] }]),
),
);
const { contactId } = props.route.params;
const paramContact = Contact.get(this.props.currentUser.id, contactId);
if (paramContact) {
this.state.action = 'edit';
}
// * Init Contact data list
const contact = paramContact
? this.initContact(paramContact)
: this.initNewContact(Contact.new());
this.state.contact = contact;
// * Setup All inputs State
this.state.allInputs = Form.getAll().map(form => {
const isDefault = form.status === InputStatus.DEFAULT;
const inputData = Object.assign({}, InputData);
inputData.title = form.placeholder[this.props.locale];
inputData.required = form.required;
inputData.formSlug = form.slug;
inputData.type = form.type;
inputData.value =
(!isDefault
? contact[form.slug]
: contact.data.find(input => input.formSlug === form.slug)?.value) ??
'';
if (form.type === InputType.CHECKBOX) {
inputData.value =
(!isDefault
? contact[form.slug]
: contact.data.find(input => input.formSlug === form.slug)
?.value) ?? [];
}
// * Init Multiple selete State
if (form.type === InputType.SELECT_MULTIPLE) {
const details = form.details.map(
detail => JSON.parse(detail)[i18n.locale] ?? null,
);
multipleSelectInputs[`input${form.id}`].selectedItems = (
inputData.value ?? []
)
.map((selectedDetail, detailIndex) => details.indexOf(selectedDetail))
.filter(detailIndex => detailIndex !== -1);
console.debug('SELECT_MULTIPLE', inputData.value);
console.debug(multipleSelectInputs[`input${form.id}`]);
}
return inputData;
});
this.state.multipleSelectInputs = multipleSelectInputs;
}
// * --- STATE ---
state = {
/** @type {'add'|'edit} */
action: 'add',
/** @type {0|1} 0 is VISITS 1, is NOTES */
selectedVisitsOrNoteTab: 0,
// * -- VISITS
selectedVisitIndex: 0,
showNewVisit: false,
tmpVisitNote: '',
newVisitTitle: '',
// * -- VCARD
/** @type {boolean|null} TRUE ok, FALSE refused, NULL not asked */
hasPermission: null,
vCardData: {
name: '',
firstname: '',
email: '',
company: '',
address: '',
phone: '',
},
/** @type {string|null} */
parsedType: null,
/** @type {string[]} */
parsedDatas: [],
/** @type {boolean} */
showQRcodevCard: false,
/** @type {boolean} */
showDynContact: false,
/** @type {boolean} */
scanned: false,
};
// * --- METHODS
// * - CONTACT METHODS
saveContact() {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = Object.assign({}, this.state.contact);
const defaultForms = Form.getAll(InputStatus.DEFAULT);
console.debug('save Contact');
let error = '';
// * VALIDATE SYSTEM FIELDS
if (
!contact.email ||
contact.email.length < 5 ||
contact.email.length >= 255 ||
!Helpers.isEmailValid(contact.email)
) {
console.debug('invalid prop email');
error = error.concat(i18n.t('error.invalid_email_address') + '.\n');
}
if (contact.firstname && contact.firstname.length >= 255) {
console.debug('invalid prop invalid_firstname');
error = error.concat(i18n.t('contact.invalid_firstname') + '.\n');
}
if (contact.lastname && contact.lastname.length >= 255) {
console.debug('invalid prop lastname');
error = error.concat(i18n.t('contact.invalid_lastname') + '.\n');
}
if (contact.phone && contact.phone.length >= 255) {
console.debug('invalid prop phone');
error = error.concat(i18n.t('contact.invalid_phone') + '.\n');
}
contact.data.forEach(input => {
const formToValidate = defaultForms.find(
form => form.slug === input.formSlug,
);
if (formToValidate.required && !input?.value?.length) {
console.debug('invalid data ', input.formSlug);
error = error.concat(
i18n.t('contact_details_s.input_required', {
placeholder: formToValidate.placeholder[i18n.locale],
}) + '.\n',
);
}
if (
input?.value?.length &&
formToValidate.type === InputType.EMAIL &&
!Helpers.isEmailValid(input.value)
) {
console.debug('invalid data email ', input.formSlug);
error = error.concat(
i18n.t('contact_details_s.input_email', {
placeholder: formToValidate.placeholder[i18n.locale],
}) + '.\n',
);
}
});
if (error.length > 0) {
Alert.alert(i18n.t('global.warning') + ' !', error, [{ text: 'OK' }], {
cancelable: false,
});
return;
}
console.debug(contact.data);
// * Stringify fields to store them
contact.visits = JSON.stringify(contact.visits);
contact.data = JSON.stringify(contact.data);
contact.timestamp = new Date();
contact.synced = false;
/** @type {setContact} */
const setContactHook = this.props.setContact;
setContactHook(this.props.currentUser, contact);
if (this.state.action === 'edit') {
showMessage({
message: i18n.t('contact_details_s.contact_saved') + '.',
type: 'success',
duration: 500,
});
} else {
setTimeout(() => {
this.props.navigation.navigate('ContactScreen', {
contactAdded: true,
});
}, 1500);
}
}
/**
*
* @param {import('./../Models/Contact').Contact} contact
* @returns {import('./../Models/Contact').ContactData}
*/
initContact(contact) {
contact = contact.toJSON();
// * Stringify fields to store them
contact.visits = JSON.parse(contact.visits);
/** @type {Map<string,import('./../Models/Contact').InputDataType>} */
const originalData = new Map(
[...JSON.parse(contact.data)].map(input => [input.formSlug, input]),
);
contact.data = Form.getAll(InputStatus.DEFAULT).map(
this.initField.bind(this),
);
console.debug(contact.data);
contact.data.map(input => {
const originalInput = originalData.get(input.formSlug);
// * Had this input before ?
if (originalInput !== undefined) {
// * Copy only if it is the same type
input.value =
input.type === originalInput.type ? originalInput.value : input.value;
}
});
return contact;
}
/**
* Get new instance of Contact
* @param {import('./../Models/Contact').ContactData} contact
* @returns {import('./../Models/Contact').ContactData}
*/
initNewContact(contact) {
// * Init DEFAULT Fields
contact.data = Form.getAll(InputStatus.DEFAULT).map(
this.initField.bind(this),
);
contact.visits = [];
contact.notes = '';
contact.app_user_id = this.props.currentUser.id;
return contact;
}
/**
* Setup input field
* @param {import('./../Models/Form').Form} form
* @returns {import('./../Models/Contact').InputDataType}
*/
initField(form) {
const inputData = Object.assign({}, InputData);
inputData.title = form.placeholder[this.props.locale];
inputData.required = form.required;
inputData.formSlug = form.slug;
inputData.type = form.type;
inputData.value = '';
if (
form.type === InputType.CHECKBOX ||
form.type === InputType.SELECT_MULTIPLE
) {
inputData.value = [];
}
return inputData;
}
// * - VISITS METHODS
newVisit() {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
contact.visits.unshift({ title: this.state.newVisitTitle, note: '' });
this.setState({
contact: contact,
showNewVisit: false,
selectedVisitIndex: 0,
});
}
deleteVisit() {
Alert.alert(
i18n.t('global.confirmation'),
i18n.t('contact_details_s.confirm_visit_delete'),
[
{
text: i18n.t('global.no'),
onPress: () => console.debug('Cancel Pressed'),
style: 'cancel',
},
{
text: i18n.t('global.yes'),
onPress: () => {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
contact.visits.splice(this.state.selectedVisitIndex, 1);
if (contact.visits.length === 0) {
this.setState({
contact: contact,
selectedVisitIndex: 0,
});
} else {
this.setState({
contact: contact,
selectedVisitIndex: 0,
});
}
},
},
],
{ cancelable: false },
);
}
// * END - VISITS METHODS
/** @type {import('expo-barcode-scanner').BarCodeScannedCallback} */
handleBarCodeScanned({ type, data }) {
this.setState({ scanned: true });
if (!data.includes('VCARD')) {
Alert.alert(i18n.t('contact_details_s.qrcode_not_vcard_format'));
return;
}
/** @type {{ }} */
let datas = vCard.parse(data);
/** @type {string[]} */
const tmpDatas = [];
if (datas.n !== undefined) {
const nx = Object.assign(
{ n1: [{ value: datas.n[0].value[0] }] },
{ n2: [{ value: datas.n[0].value[1] }] },
);
datas = Object.assign(datas, nx);
}
if (datas.adr !== undefined) {
const tmp = datas.adr[0].value;
let tmpadr = '';
for (let i = 0; i < tmp.length; i++) {
if (tmp[i].length > 0) {
if (tmpadr.length > 0) {
tmpadr = tmpadr + ', ' + tmp[i];
} else {
tmpadr = tmp[i];
}
}
}
datas.adr = [{ value: tmpadr }];
}
Object.keys(datas).map((objectKey, index) => {
if (
objectKey === 'email' ||
objectKey === 'adr' ||
objectKey === 'tel' ||
objectKey === 'org' ||
objectKey === 'n1' ||
objectKey === 'n2'
) {
let value = datas[objectKey][0].value;
if (typeof value === 'string') {
tmpDatas.push(value);
}
}
});
this.setState({
parsedType: 'vcard',
parsedDatas: tmpDatas,
showQRcodevCard: false,
showDynContact: true,
});
}
// * END --- METHODS
componentDidMount() {
const { action } = this.props.route.params;
this.setState({ action: action === 'edit' ? 'edit' : 'add' });
BarCodeScanner.requestPermissionsAsync().then(res => {
this.setState({ hasPermission: res.status === 'granted' });
});
}
render() {
return (
<View style={styles.container}>
{/* LEFT SIDE -- FORMULARY VIEW */}
<ScrollView style={{ flex: 1 }}>
{/* ALL INPUTS : They are sorted on sync by server */}
{this.renderFormulary(Form.getAll()).map((input, inputIndex) => {
input.key = inputIndex;
return input;
})}
{/* BOTTOM SPACE */}
<View style={{ height: 50 }} />
</ScrollView>
{/* RIGHT SIDE -- VISITS AND NOTES */}
<View style={{ flex: 1, margin: 5 }}>
{/* VISITS AND NOTES */}
<View style={{ flex: 1 }}>
<ButtonGroup
buttons={[
i18n.t('contact_details_s.visits'),
i18n.t('contact_details_s.notes'),
]}
selectedButtonStyle={{ backgroundColor: this.props.mainColor }}
containerStyle={{ height: 30 }}
selectedIndex={this.state.selectedVisitsOrNoteTab}
onPress={index =>
this.setState({ selectedVisitsOrNoteTab: index })
}
/>
{/* VISITS */}
{this.state.selectedVisitsOrNoteTab === 0 && (
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row' }}>
{this.state.contact.visits.length > 0 && (
<View style={{ flex: 2, flexDirection: 'row' }}>
<FontAwesome5
name="sort-down"
size={20}
color={'black'}
style={{
zIndex: 1,
position: 'absolute',
right: 28,
top: 22,
}}
/>
<Picker
style={styles.select}
selectedValue={this.state.selectedVisitIndex}
onValueChange={(itemValue, itemIndex) => {
this.setState({
selectedVisitIndex: itemIndex,
});
}}>
{this.state.contact.visits.map((item, index) => {
return (
<Picker.Item
label={item.title}
value={index}
key={index}
/>
);
})}
</Picker>
</View>
)}
{this.state.contact.visits.length === 0 && (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>{i18n.t('contact_details_s.no_visits')}.</Text>
</View>
)}
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-evenly',
margin: 10,
}}>
<TouchableOpacity
style={styles.button}
onPress={() => {
const localDatetimeString = new Date().toLocaleString(
this.props.locale,
);
this.setState({
showNewVisit: true,
newVisitTitle: localDatetimeString.substring(
0,
localDatetimeString.length - 3,
),
});
}}>
<FontAwesome5
name="plus-circle"
size={20}
color={this.props.mainColor}
/>
</TouchableOpacity>
{this.state.contact.visits.length > 0 && (
<TouchableOpacity
style={styles.button}
onPress={() => this.deleteVisit()}>
<FontAwesome5
name="trash"
size={20}
color={this.props.mainColor}
/>
</TouchableOpacity>
)}
</View>
</View>
{this.state.contact.visits.length > 0 && (
<TextInput
style={styles.textArea}
underlineColorAndroid="transparent"
placeholder={i18n.t('contact_details_s.notes') + '...'}
placeholderTextColor="grey"
multiline={true}
value={
this.state.contact.visits[this.state.selectedVisitIndex]
.note
}
onChangeText={text => {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
contact.visits[this.state.selectedVisitIndex].note = text;
this.setState({ contact: contact });
}}
/>
)}
</View>
)}
{/* NOTES */}
{this.state.selectedVisitsOrNoteTab === 1 && (
<TextInput
style={styles.textArea}
underlineColorAndroid="transparent"
placeholder={i18n.t('contact_details_s.notes') + '...'}
placeholderTextColor="grey"
multiline={true}
value={this.state.contact.notes}
onChangeText={text => {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
contact.notes = text;
this.setState({ contact: contact });
}}
/>
)}
</View>
{/* ACTIONS BUTTONS on BOTTOM */}
<View
style={{ flexDirection: 'row', justifyContent: 'space-evenly' }}>
{this.state.action === 'add' && (
<Button
title="QRCode vCard"
onPress={() => this.setState({ showQRcodevCard: true })}
color={this.props.mainColor}
// THIS IS NOT AVAILABLE FOR NOW
disabled={true}
/>
)}
<Button
title={i18n.t('global.save')}
onPress={() => this.saveContact()}
color={this.props.mainColor}
/>
</View>
</View>
{/* NEW VISIT OVERLAY */}
<Overlay
isVisible={this.state.showNewVisit}
windowBackgroundColor="rgba(255, 255, 255, .9)"
overlayBackgroundColor="white"
width={400}
height={300}>
<View
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ marginBottom: 10 }}>
{i18n.t('contact_details_s.visit_name')} :
</Text>
<TextInput
placeholder={i18n.t('contact_details_s.visit_name')}
value={this.state.newVisitTitle}
onChangeText={text => this.setState({ newVisitTitle: text })}
style={{
borderWidth: 1,
borderColor: 'lightgrey',
width: 300,
marginBottom: 15,
paddingLeft: 10,
}}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
width: 400,
}}>
<Button
title={i18n.t('global.cancel')}
onPress={() => this.setState({ showNewVisit: false })}
color={this.props.mainColor}
/>
<Button
title={i18n.t('global.add')}
onPress={() => this.newVisit()}
color={this.props.mainColor}
/>
</View>
</View>
</Overlay>
{/* VCARD QR CODE OVERLAY -- TAKE PICTURE */}
<Overlay
isVisible={this.state.showQRcodevCard}
windowBackgroundColor="rgba(255, 255, 255, .9)"
overlayBackgroundColor="white"
width={400}
height={300}>
<View
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
{this.state.hasPermission === true && (
<BarCodeScanner
onBarCodeScanned={
this.state.scanned ? undefined : this.handleBarCodeScanned
}
barCodeTypes={[BarCodeScanner.Constants.BarCodeType.qr]}
style={{ flex: 1, width: 380, height: 200 }}
/>
)}
{this.state.hasPermission === false && (
<Text>{i18n.t('error.camera_permission_denied')}.</Text>
)}
{this.state.hasPermission === null && (
<Text>{i18n.t('error.pending_camera_permission')}.</Text>
)}
<View
style={{
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
width: 400,
}}>
<Button
title={i18n.t('global.cancel')}
onPress={() =>
this.setState({ showQRcodevCard: false, scanned: false })
}
color={this.props.mainColor}
/>
{this.state.scanned && (
<Button
title={i18n.t('global.retry')}
onPress={() => this.setState({ scanned: false })}
color={this.props.mainColor}
/>
)}
</View>
</View>
</Overlay>
{/* VCARD QR CODE OVERLAY -- SCAN RESULT */}
<Overlay
isVisible={this.state.showDynContact}
windowBackgroundColor="rgba(255, 255, 255, .9)"
overlayBackgroundColor="white"
width={400}
height={400}>
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly',
}}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.lastname')}
</Text>
<Picker
selectedValue={this.state.vCardData.name}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) => {
const vCardData = this.state.vCardData;
vCardData.name = itemValue;
this.setState({ vCardData: vCardData });
}}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.firstname')}
</Text>
<Picker
selectedValue={this.state.parsedFirstname}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) =>
this.setState({ parsedFirstname: itemValue })
}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.email')}
</Text>
<Picker
selectedValue={this.state.parsedEmail}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) =>
this.setState({ parsedEmail: itemValue })
}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.company')}
</Text>
<Picker
selectedValue={this.state.parsedCompany}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) =>
this.setState({ parsedCompany: itemValue })
}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.address')}
</Text>
<Picker
selectedValue={this.state.parsedAddress}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) =>
this.setState({ parsedAddress: itemValue })
}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 1, paddingLeft: 10 }}>
{i18n.t('global.phone')}
</Text>
<Picker
selectedValue={this.state.parsedPhone}
style={{ height: 50, width: 250 }}
onValueChange={(itemValue, itemIndex) =>
this.setState({ parsedPhone: itemValue })
}>
<Picker.Item label="" value="0" key={0} />
{this.state.parsedType === 'vcard' &&
this.state.parsedDatas.map((item, index) => {
return (
<Picker.Item
label={item}
value={item}
key={(index + 1).toString()}
/>
);
})}
</Picker>
</View>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
width: 400,
}}>
<Button
title={i18n.t('global.cancel')}
onPress={() => {
this.setState({
showDynContact: false,
scanned: false,
vCard: {
name: '',
firstname: '',
email: '',
company: '',
address: '',
phone: '',
},
});
}}
color={this.props.mainColor}
/>
<Button
title={i18n.t('global.next')}
onPress={() => {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
contact.lastname = this.state.vCardData.name;
contact.firstname = this.state.vCardData.firstname;
contact.email = this.state.vCardData.email;
contact.phone = this.state.vCardData.phone;
// FIXME: Insert other fields from DATA.
this.setState({
contact: contact,
vCardData: {
name: '',
firstname: '',
email: '',
company: '',
address: '',
phone: '',
},
showDynContact: false,
scanned: false,
});
}}
color={this.props.mainColor}
/>
</View>
</View>
</Overlay>
</View>
);
}
/// * --- RENDER METHODS
/**
* @param {Realm.Results<import('./../Models/Form').Form>} formularies
* @returns {JSX.Element[]}
*/
renderFormulary(formularies) {
/** @type {JSX.Element} */
const inputs = [];
[...formularies.values()]
.sort((a, b) => a - b)
.forEach((form, inputIndex) => {
/** @type {string|null} */
const placeHolder = form.placeholder[i18n.locale] ?? null;
const requiredStr = form.required
? ` (${i18n.t('global.required')})`
: '';
/** @type {string[]} Already translated details */
const details = form.details
.map(detail => JSON.parse(detail)[i18n.locale] ?? null)
.filter(detail => detail !== null);
switch (form.type) {
case InputType.TEXT:
inputs.push(
this.renderTextInput(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
'default',
form.is_visible,
),
);
break;
case InputType.EMAIL:
inputs.push(
this.renderTextInput(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
'email-address',
form.is_visible,
),
);
break;
case InputType.PHONE:
inputs.push(
this.renderTextInput(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
'phone-pad',
form.is_visible,
),
);
break;
case InputType.CHECKBOX:
inputs.push(
this.renderCheckBox(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
details,
form.is_visible,
),
);
break;
case InputType.RADIO:
inputs.push(
this.renderRadio(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
details,
form.is_visible,
),
);
break;
case InputType.SELECT:
inputs.push(
this.renderSelect(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
details,
form.is_visible,
),
);
break;
case InputType.SELECT_MULTIPLE:
inputs.push(
this.renderSelectMultiple(
placeHolder,
requiredStr,
inputIndex,
form.slug,
form.status === InputStatus.DEFAULT,
details,
form,
form.is_visible,
),
);
break;
default:
throw new Error(
`Unhandled input type '${form.type}' cannot render input`,
);
}
});
return inputs;
}
/**
* Render a TextInput
*
* @param {string} placeHolder The place holder text to display
* @param {string} requiredStr The required text to display
* @param {number} index The input index (formulary order)
* @param {string} slug The formulary input name
* @param {boolean} isDefault The input status is Default ? (if no it is a prop on contact model)
* @param {string} keyboardType Keyboard type to disaply on user
* @param {boolean} isVisible The input is Visible
* @returns
*/
renderTextInput(
placeHolder,
requiredStr,
index,
slug,
isDefault,
keyboardType,
isVisible,
) {
return this.wrapper(
placeHolder,
requiredStr,
<TextInput
value={this.state.allInputs[index].value}
onChangeText={text => {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
if (isDefault) {
const dataIndex = contact.data.findIndex(
input => input.formSlug === slug,
);
// * Set value on data.
contact.data[dataIndex].value = text;
console.debug('data ', contact.data[dataIndex].value);
} else {
// * Set value on model
contact[slug] = text;
console.debug('prop ', contact[slug]);
}
/** @type {Array<import('./../Models/Contact').InputDataType>} */
const allInputs = this.state.allInputs;
allInputs[index].value = text;
this.setState({
contact: contact,
allInputs: allInputs,
});
}}
style={{ ...styles.input, ...{ display: isVisible ? 'flex' : 'none' } }}
placeholder={`${placeHolder} ${requiredStr}`}
autoCapitalize={'none'}
autoCorrect={false}
keyboardType={keyboardType}
/>,
);
}
/**
* Render a Checkbox Input (one checkbox per choice)
*
* @param {string} placeHolder The place holder text to display
* @param {string} requiredStr The required text to display
* @param {number} index The input index (formulary order)
* @param {string} slug The formulary input name
* @param {boolean} isDefault The input status is Default ? (if no it is a prop on contact model)
* @param {string[]} details The details associated with the formulary input
* @param {boolean} isVisible The input is Visible
* @returns
*/
renderCheckBox(
placeHolder,
requiredStr,
index,
slug,
isDefault,
details,
isVisible,
) {
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
/** @type {Array<import('./../Models/Contact').InputDataType>} */
const allInputs = this.state.allInputs;
/** @type {string[]} */
const checkedValues = (allInputs[index].value ?? []).filter(
checkBoxValue => {
// * Filter values to remove props that does not exists
return details.indexOf(checkBoxValue) !== -1;
},
);
return this.wrapper(
placeHolder,
requiredStr,
<View
style={{
display: isVisible ? 'flex' : 'none',
flexDirection: 'row',
flexWrap: 'wrap',
width: '100%',
}}>
{details.map((detail, detailIndex) => {
/** The index of the checkbox value */
const indexValue = checkedValues.indexOf(detail);
return (
<CheckBox
checked={
checkedValues.findIndex(
checkBoxValue => checkBoxValue === detail,
) !== -1
}
onPress={() => {
if (indexValue !== -1) {
checkedValues.splice(indexValue, 1);
} else {
checkedValues.push(detail);
}
// * Filter Legacy details to conserve order
const newCheckedValues = details.filter(
legacyDetail => checkedValues.indexOf(legacyDetail) !== -1,
);
console.debug(newCheckedValues);
if (isDefault) {
const dataIndex = contact.data.findIndex(
input => input.formSlug === slug,
);
// * Set value on data.
contact.data[dataIndex].value = newCheckedValues;
console.debug('data ', contact.data[dataIndex]);
} else {
// * Set value on model
contact[slug] = newCheckedValues;
console.debug('prop ', contact[slug]);
}
allInputs[index].value = newCheckedValues;
this.setState({
contact: contact,
allInputs: allInputs,
});
}}
key={detailIndex}
checkedColor={this.props.mainColor}
center
title={detail}
containerStyle={{
maxWidth: 350,
}}
/>
);
})}
</View>,
);
}
/**
* Render a Select Multiple Input
*
* @param {string} placeHolder The place holder text to display
* @param {string} requiredStr The required text to display
* @param {number} index The input index (formulary order)
* @param {string} slug The formulary input name
* @param {boolean} isDefault The input status is Default ? (if no it is a prop on contact model)
* @param {string[]} details The details associated with the formulary input
* @param {import('./../Models/Form').Form} form The Form input
* @param {boolean} isVisible The input is Visible
* @returns
*/
renderSelectMultiple(
placeHolder,
requiredStr,
index,
slug,
isDefault,
details,
form,
isVisible,
) {
const multipleSelectInputs = this.state.multipleSelectInputs;
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
return this.wrapper(
placeHolder,
requiredStr,
<View
style={{
display: isVisible ? 'flex' : 'none',
flex: 1,
flexDirection: 'column',
flexGrow: 1,
backgroundColor: 'white',
margin: 10,
}}>
<SectionedMultiSelect
searchPlaceholderText={i18n.t(
'contact_details_s.input_select_multiple_search',
)}
confirmText={i18n.t('global.close')}
items={details.map(function (detail, detailIndex) {
return {
id: detailIndex,
name: detail,
};
})}
IconRenderer={this.getIcon}
uniqueKey="id"
selectText={`${placeHolder} ${requiredStr}`}
showDropDowns={false}
showChips={false}
modalWithTouchable={true}
alwaysShowSelectText={true}
renderSelectText={props =>
props.selectedItems
.map(detailIndex => details[detailIndex])
.join(', ')
}
onSelectedItemsChange={items => {
/** @var number[] */
const selectedItems = items;
// * Filter Legacy details to conserve order
const selectedItemsLabels = details.filter(
(legacyDetail, legacyDetailIndex) =>
selectedItems.indexOf(legacyDetailIndex) !== -1,
);
// * Update Selected State
multipleSelectInputs[`input${form.id}`].selectedItems =
selectedItems;
console.debug('IS DEfault', isDefault ? 'true' : 'false');
console.debug('selected Items', selectedItems);
console.debug('selected Items labels', selectedItemsLabels);
if (isDefault) {
const dataIndex = contact.data.findIndex(
input => input.formSlug === slug,
);
// * Set value on data.
contact.data[dataIndex].value = selectedItemsLabels;
console.debug('data ', contact.data[dataIndex].value);
} else {
// * Set value on model
contact[slug] = selectedItemsLabels;
console.debug('prop ', contact[slug]);
}
this.setState({
multipleSelectInputs: multipleSelectInputs,
contact: contact,
});
}}
selectedItems={multipleSelectInputs[`input${form.id}`].selectedItems}
icons={{
search: {
name: 'search',
size: 24,
style: {
fa: 'search',
left: 10,
},
},
arrowUp: {
name: 'keyboard-arrow-up',
size: 22,
style: {
fa: 'sort-up',
},
},
arrowDown: {
name: 'keyboard-arrow-down',
size: 22,
style: {
fa: 'sort-down',
},
},
selectArrowDown: {
name: 'keyboard-arrow-down',
size: 20,
style: {
fa: 'sort-down',
right: 20,
},
},
close: {
name: 'close',
size: 16,
style: {
fa: 'window-close',
},
},
check: {
name: 'check-circle',
size: 24,
style: {
fa: 'check-circle',
position: 'absolute',
left: 0,
top: 20,
},
},
cancel: {
name: 'cancel',
size: 18,
style: {
fa: 'times',
},
},
}}
styles={{
button: {
backgroundColor: this.props.mainColor,
maxWidth: 150,
margin: 20,
left: '80%',
},
searchTextInput: {
paddingLeft: 20,
},
container: {
paddingTop: 20,
width: '90%',
height: '60%',
flex: 0,
alignSelf: 'center',
justifyContent: 'center',
top: '10%',
},
selectToggle: {
marginTop: 7,
height: 50,
},
selectToggleText: {
fontSize: 16,
marginStart: 13,
marginTop: 15,
},
separator: {
color: 'white',
borderWidth: 0,
display: 'none',
},
item: {
paddingTop: 20,
paddingBottom: 20,
width: '100%',
},
selectedItem: {
paddingTop: 20,
paddingBottom: 20,
},
selectedItemText: {
fontStyle: 'italic',
left: 40,
},
}}
/>
</View>,
);
}
/**
* Render a Select Input
*
* @param {string} placeHolder The place holder text to display
* @param {string} requiredStr The required text to display
* @param {number} index The input index (formulary order)
* @param {string} slug The formulary input name
* @param {boolean} isDefault The input status is Default ? (if no it is a prop on contact model)
* @param {string[]} details The details associated with the formulary input
* @param {boolean} isVisible The input is Visible
* @returns
*/
renderSelect(
placeHolder,
requiredStr,
index,
slug,
isDefault,
details,
isVisible,
) {
/** String separator for input value */
// const separator = ', ';
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
/** @type {Array<import('./../Models/Contact').InputDataType>} */
const allInputs = this.state.allInputs;
const selected = allInputs[index].value ?? '';
// * Make sure the actual option is available
const filteredSelected =
details.findIndex(legacyDetail => legacyDetail === selected) ?? -1;
return this.wrapper(
placeHolder,
requiredStr,
<View
style={{
display: isVisible ? 'flex' : 'none',
flex: 1,
flexDirection: 'row',
}}>
<FontAwesome5
name="sort-down"
size={20}
color={'black'}
style={{
zIndex: 1,
position: 'absolute',
right: 28,
top: 22,
}}
/>
<Picker
selectedValue={filteredSelected}
onValueChange={itemIndex => {
if (isDefault) {
const dataIndex = contact.data.findIndex(
input => input.formSlug === slug,
);
console.debug(dataIndex);
console.debug(contact.data);
// * Set value on data.
contact.data[dataIndex].value = details[itemIndex] ?? undefined;
console.debug(contact.data[dataIndex]);
} else {
// * Set value on model
contact[slug] = details[itemIndex] ?? undefined;
console.debug(contact[slug]);
}
allInputs[index].value = details[itemIndex] ?? undefined;
this.setState({
contact: contact,
allInputs: allInputs,
});
}}
style={styles.select}>
<Picker.Item
key={-1}
label={i18n.t('contact_details_s.input_select_please_choose')}
value={''}
/>
{details.map((detail, detailIndex) => (
<Picker.Item key={detailIndex} label={detail} value={detailIndex} />
))}
</Picker>
</View>,
);
}
/**
* Render a Radio Input
*
* @param {string} placeHolder The place holder text to display
* @param {string} requiredStr The required text to display
* @param {number} index The input index (formulary order)
* @param {string} slug The formulary input name
* @param {boolean} isDefault The input status is Default ? (if no it is a prop on contact model)
* @param {string[]} details The details associated with the formulary input
* @param {boolean} isVisible The input is Visible
* @returns
*/
renderRadio(
placeHolder,
requiredStr,
index,
slug,
isDefault,
details,
isVisible,
) {
/** String separator for input value */
// const separator = ', ';
/** @type {typeof import('./../Models/Contact').ContactData} */
const contact = this.state.contact;
/** @type {Array<import('./../Models/Contact').InputDataType>} */
const allInputs = this.state.allInputs;
const selected = String(this.state.allInputs[index].value) ?? '';
// * Make sure the actual option is available
const filteredSelected =
details.findIndex(legacyDetail => legacyDetail === selected) ?? -1;
return this.wrapper(
placeHolder,
requiredStr,
<View
style={{
display: isVisible ? 'flex' : 'none',
flexDirection: 'row',
flexWrap: 'wrap',
width: '100%',
}}>
{details.map((detail, detailIndex) => (
<CheckBox
checked={filteredSelected === detailIndex}
onPress={() => {
if (isDefault) {
const dataIndex = contact.data.findIndex(
input => input.formSlug === slug,
);
// * Set value on data.
contact.data[dataIndex].value = detail;
console.debug(contact.data[dataIndex]);
} else {
// * Set value on model
contact[slug] = detail;
console.debug(contact[slug]);
}
allInputs[index].value = detail;
this.setState({
contact: contact,
allInputs: allInputs,
});
}}
key={detailIndex}
checkedColor={this.props.mainColor}
center
title={detail}
checkedIcon="dot-circle-o"
uncheckedIcon="circle-o"
containerStyle={
(styles.clearRadio,
{
maxWidth: 350,
})
}
/>
))}
</View>,
);
}
/**
* wrap input
* @param {string} placeHolder
* @param {string} requiredStr
* @param {JSX.Element} el
* @returns {JSX.Element}
*/
wrapper(placeHolder, requiredStr, el) {
return (
<View style={{ flex: 1, flexDirection: 'column' }}>
<Text
style={
(styles.baseText,
{ paddingTop: 25, paddingStart: 10, fontSize: 16 })
}>{`${placeHolder} ${requiredStr}`}</Text>
{el}
</View>
);
}
/** Icon Builder for Select Multi */
getIcon({ name, size = 18, style }) {
// flatten the styles
const flat = StyleSheet.flatten(style);
// remove out the keys that aren't accepted on View
const { color, fontSize, fa, ...styles } = flat;
return name ? (
<FontAwesome5 name={fa} size={size} color={color} style={styles} />
) : (
<View />
);
}
/// * END --- RENDER METHODS
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ContactDetailsScreen);