refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,425 @@
<template>
<div class="sidebar-box" :class="[ editMode ? 'edit' : '' ]">
<div class="w-100 dt">
<div class="sidebar-box-title">
<h3>
{{ $t('people.contact_address_title') }}
</h3>
</div>
<div v-if="contactAddresses.length > 0" class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<a v-if="!editMode" class="pointer" href="" @click.prevent="editMode = true">
{{ $t('app.edit') }}
</a>
<a v-else class="pointer" href="" @click.prevent="toggleEditExcept(-1); resetState();">
{{ $t('app.done') }}
</a>
</div>
</div>
<!-- EMPTY BOX - DISPLAY ADD BUTTON -->
<p v-if="contactAddresses.length === 0 && !addMode" class="mb0">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</p>
<!-- LIST OF ADDRESSES -->
<ul v-if="contactAddresses.length > 0">
<li v-for="(contactAddress, i) in contactAddresses" :key="contactAddress.id" class="mb2">
<div v-show="!contactAddress.edit" class="w-100 dt">
<div class="dtc">
<em class="f6 light-silver fa fa-globe pr2"></em>
<a v-if="!editMode" :href="contactAddress.googleMapAddress" target="_blank" rel="noopener noreferrer">
{{ contactAddress.address }}
</a>
<span v-else>
{{ contactAddress.address }}
</span>
<span v-if="contactAddress.name" class="light-silver">
({{ contactAddress.name }})
</span>
<span v-if="!contactAddress.address">
<a v-if="contactAddress.latitude" :href="contactAddress.googleMapAddressLatitude" target="_blank" rel="noopener noreferrer">
({{ contactAddress.latitude }}, {{ contactAddress.longitude }})
</a>
</span>
<div v-if="editMode" class="fr">
<em class="fa fa-pencil-square-o pointer pr2" @click="toggleEdit(contactAddress)"></em>
<em class="fa fa-trash-o pointer" @click="trash(contactAddress)"></em>
</div>
</div>
</div>
<!-- EDIT BOX -->
<div v-show="contactAddress.edit" class="w-100">
<form class="measure center" @keyup.enter="update(contactAddress)">
<div class="mt3">
<form-input
:id="'name' + i"
v-model="updateForm.name"
:title="$t('people.contact_address_form_name')"
input-type="text"
:required="false"
:iclass="'pa2 db w-100'"
/>
</div>
<div class="mt3">
<form-input
:id="'street' + i"
v-model="updateForm.street"
:title="$t('people.contact_address_form_street')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
:id="'city' + i"
v-model="updateForm.city"
:title="$t('people.contact_address_form_city')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
:id="'province' + i"
v-model="updateForm.province"
:title="$t('people.contact_address_form_province')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
:id="'postal_code' + i"
v-model="updateForm.postal_code"
:title="$t('people.contact_address_form_postal_code')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-select
:id="'name' + i"
v-model="updateForm.country"
:title="$t('people.contact_address_form_country')"
:options="countries"
:required="false"
/>
</div>
<div class="mt3">
<form-input
:id="'latitude' + i"
v-model="updateForm.latitude"
:title="$t('people.contact_address_form_latitude')"
input-type="number"
step="0.0000001"
:required="false"
/>
</div>
<div class="mt3">
<form-input
:id="'longitude' + i"
v-model="updateForm.longitude"
:title="$t('people.contact_address_form_longitude')"
input-type="number"
step="0.0000001"
:required="false"
/>
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="update(contactAddress)">
{{ $t('app.save') }}
</a>
<a class="btn" href="" @click.prevent="toggleEdit(contactAddress)">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</li>
<!-- ADD BUTTON ONLY WHEN EDIT MODE IS AVAILABLE -->
<li v-if="editMode && !addMode">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</li>
</ul>
<!-- ADD NEW ADDRESS -->
<div v-if="addMode">
<form class="measure center" @keyup.enter="store">
<div class="mt3">
<form-input
id="name"
v-model="createForm.name"
:title="$t('people.contact_address_form_name')"
input-type="text"
:required="false"
:iclass="'pa2 db w-100'"
/>
</div>
<div class="mt3">
<form-input
id="street"
v-model="createForm.street"
:title="$t('people.contact_address_form_street')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
id="city"
v-model="createForm.city"
:title="$t('people.contact_address_form_city')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
id="province"
v-model="createForm.province"
:title="$t('people.contact_address_form_province')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-input
id="postal_code"
v-model="createForm.postal_code"
:title="$t('people.contact_address_form_postal_code')"
input-type="text"
:required="false"
/>
</div>
<div class="mt3">
<form-select
id="name"
v-model="createForm.country"
:title="$t('people.contact_address_form_country')"
:options="countries"
:required="false"
/>
</div>
<div class="mt3">
<form-input
id="latitude"
v-model="createForm.latitude"
:title="$t('people.contact_address_form_latitude')"
input-type="number"
step="0.0000001"
:required="false"
/>
</div>
<div class="mt3">
<form-input
id="longitude"
v-model="createForm.longitude"
:title="$t('people.contact_address_form_longitude')"
input-type="number"
step="0.0000001"
:required="false"
/>
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.add') }}
</a>
<a class="btn" href="" @click.prevent="resetState">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
},
data() {
return {
contactAddresses: [],
countries: [],
editMode: false,
addMode: false,
createForm: {
name: '',
street: '',
city: '',
province: '',
postal_code: '',
country: '',
latitude: 0,
longitude: 0,
},
updateForm: {
id: '',
name: '',
street: '',
city: '',
province: '',
postal_code: '',
country: '',
latitude: 0,
longitude: 0,
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getAddresses();
this.getCountries();
},
getAddresses() {
axios.get('people/' + this.hash + '/addresses')
.then(response => {
this.contactAddresses = response.data;
});
},
getCountries() {
axios.get('countries')
.then(response => {
this.countries = _.map(response.data, function(country) {
return {
id: country.id,
name: country.country
};
});
});
},
reinitialize() {
this.createForm.name = '';
this.createForm.street = '';
this.createForm.city = '';
this.createForm.province = '';
this.createForm.postal_code = '';
this.createForm.country = '';
this.createForm.latitude = '';
this.createForm.longitude = '';
},
resetState() {
this.editMode = false;
this.addMode = false;
},
toggleAdd() {
this.addMode = true;
this.editMode = true;
this.reinitialize();
},
toggleEdit(contactAddress) {
this.addMode = false;
this.toggleEditExcept(contactAddress.id);
Vue.set(contactAddress, 'edit', !contactAddress.edit);
this.updateForm.id = contactAddress.id;
this.updateForm.name = contactAddress.name;
this.updateForm.street = contactAddress.street;
this.updateForm.city = contactAddress.city;
this.updateForm.province = contactAddress.province;
this.updateForm.postal_code = contactAddress.postal_code;
this.updateForm.country = contactAddress.country;
this.updateForm.latitude = contactAddress.latitude;
this.updateForm.longitude = contactAddress.longitude;
},
toggleEditExcept(contactAddressId) {
_.forEach(_.filter(this.contactAddresses, function (a) {
return a.id !== contactAddressId;}
), function (a) {
Vue.set(a, 'edit', false);
});
},
store() {
var vm = this;
this.persistClient(
'post', 'people/' + this.hash + '/addresses',
this.createForm
).then(response => {
vm.contactAddresses.push(response.data);
});
this.addMode = false;
},
update(contactAddress) {
var vm = this;
Vue.set(contactAddress, 'edit', !contactAddress.edit);
this.persistClient(
'put', 'people/' + this.hash + '/addresses/' + contactAddress.id,
this.updateForm
).then(response => {
Vue.set(vm.contactAddresses, vm.contactAddresses.indexOf(vm.contactAddresses.find(item => item.id === response.data.id)), response.data);
});
},
trash(contactAddress) {
var vm = this;
this.updateForm.id = contactAddress.id;
this.persistClient(
'delete', 'people/' + this.hash + '/addresses/' + contactAddress.id,
this.updateForm
).then(response => {
if (response.data.deleted === true) {
vm.contactAddresses.splice(vm.contactAddresses.indexOf(vm.contactAddresses.find(item => item.id === response.data.id)), 1);
}
});
if (this.contactAddresses.length <= 1) {
this.editMode = false;
}
},
persistClient(method, uri, form) {
form.errors = {};
return axios[method](uri, form)
.catch(error => {
if (typeof error.response.data === 'object') {
form.errors = _.flatten(_.toArray(error.response.data));
} else {
form.errors = [this.$t('app.error_try_again')];
}
});
},
}
};
</script>

View File

@@ -0,0 +1,66 @@
<style scoped>
.fa {
top: 1px;
color: #b1b1b1;
}
</style>
<template>
<div>
<notifications group="archive" position="bottom right" :duration="5000" width="400" />
<a class="pointer" :title="$t('people.contact_archive_help')" href="" @click.prevent="toggle">
{{ isActive ? $t('people.contact_archive') : $t('people.contact_unarchive') }}
</a>
<span v-tooltip.top="$t('people.contact_archive_help')">
<em class="fa fa-info-circle relative pointer"></em>
</span>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
active: {
type: Boolean,
default: true,
},
},
data() {
return {
isActive: false,
};
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.isActive = this.active;
},
toggle() {
axios.put('people/' + this.hash + '/archive')
.then(response => {
this.isActive = response.data.is_active;
this.$notify({
group: 'archive',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
}
};
</script>

View File

@@ -0,0 +1,260 @@
<template>
<div class="sidebar-box" :class="[ editMode ? 'edit' : '' ]">
<div class="w-100 dt">
<div class="sidebar-box-title">
<h3>
{{ $t('people.contact_info_title') }}
</h3>
</div>
<div v-if="contactInformationData.length > 0" class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<a v-if="!editMode" class="pointer" href="" @click.prevent="editMode = true">
{{ $t('app.edit') }}
</a>
<a v-else class="pointer" href="" @click.prevent="resetState">
{{ $t('app.done') }}
</a>
</div>
</div>
<p v-if="contactInformationData.length === 0 && !addMode" class="mb0">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</p>
<ul v-if="contactInformationData.length > 0">
<li v-for="contactInformation in contactInformationData" :key="contactInformation.id" class="mb2">
<div v-show="!contactInformation.edit" class="w-100 dt">
<div class="dtc">
<em v-if="contactInformation.fontawesome_icon" :class="contactInformation.fontawesome_icon" class="pr2 f6 light-silver"></em>
<em v-else class="pr2 fa fa-address-card-o f6 gray"></em>
<a v-if="contactInformation.protocol" :href="contactInformation.protocol + contactInformation.data">
{{ contactInformation.shortenName }}
</a>
<a v-else-if="contactInformation.data.indexOf('://') !== -1" :href="contactInformation.data">
{{ contactInformation.shortenName }}
</a>
<span v-else>
{{ contactInformation.shortenName }}
</span>
</div>
<div v-if="editMode" class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<em class="fa fa-pencil-square-o pointer pr2" @click="toggleEdit(contactInformation)"></em>
<em class="fa fa-trash-o pointer" @click="trash(contactInformation)"></em>
</div>
</div>
<div v-show="contactInformation.edit" class="w-100">
<form class="measure center" @submit.prevent="update(contactInformation)">
<div class="mt3">
<form-input
id="contact-content"
v-model="updateForm.data"
:title="$t('people.contact_info_form_content')"
iclass="pa2 db w-100"
:input-type="'text'"
/>
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="update(contactInformation)">
{{ $t('app.save') }}
</a>
<a class="btn" href="" @click.prevent="toggleEdit(contactInformation)">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</li>
<li v-if="editMode && !addMode">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</li>
</ul>
<div v-if="addMode">
<form class="measure center" @submit.prevent="store">
<div class="mt3">
<label for="add-contact-type" class="db fw6 lh-copy f6">
{{ $t('people.contact_info_form_contact_type') }} <a class="fr normal" href="settings/personalization" target="_blank">
{{ $t('people.contact_info_form_personalize') }}
</a>
</label>
<select id="add-contact-type" v-model="createForm.contact_field_type_id" class="db w-100 h2">
<option v-for="contactFieldType in contactFieldTypes" :key="contactFieldType.id" :value="contactFieldType.id">
{{ contactFieldType.name }}
</option>
</select>
</div>
<div class="mt3">
<label class="db fw6 lh-copy f6">
{{ $t('people.contact_info_form_content') }}
</label>
<input v-model="createForm.data" class="pa2 db w-100" type="text" />
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.add') }}
</a>
<a class="btn" href="" @click.prevent="resetState">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: -1,
},
sizeLimit: {
type: Number,
default: 26,
}
},
data() {
return {
contactInformationData: [],
contactFieldTypes: [],
editMode: false,
addMode: false,
updateMode: false,
createForm: {
contact_field_type_id: '',
data: '',
errors: []
},
updateForm: {
id: '',
contact_field_type_id: '',
data: '',
edit: false,
errors: []
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getContactInformationData();
this.getContactFieldTypes();
},
getContactInformationData() {
axios.get('people/' + this.hash + '/contactfield')
.then(response => {
this.contactInformationData = this.formatResponse(response.data);
});
},
formatResponse(data) {
var vm = this;
_.each(data, function(value) {
var shortenName = value.data;
if (shortenName.length > vm.sizeLimit + 1) {
shortenName = vm.$t('format.short_text', { text: shortenName.substr(0, vm.sizeLimit) });
}
value.shortenName = shortenName;
});
return data;
},
getContactFieldTypes() {
axios.get('people/' + this.hash + '/contactfieldtypes')
.then(response => {
this.contactFieldTypes = response.data;
});
},
store() {
this.persistClient(
'post', 'people/' + this.hash + '/contactfield',
this.createForm
);
this.addMode = false;
},
resetState() {
this.editMode = false;
this.addMode = false;
},
toggleAdd() {
this.addMode = true;
this.editMode = true;
this.createForm.data = '';
this.createForm.contact_field_type_id = '';
},
toggleEdit(contactField) {
Vue.set(contactField, 'edit', !contactField.edit);
this.updateForm.id = contactField.id;
this.updateForm.data = contactField.data;
this.updateForm.contact_field_type_id = contactField.contact_field_type_id;
},
update(contactField) {
this.persistClient(
'put', 'people/' + this.hash + '/contactfield/' + contactField.id,
this.updateForm
);
},
trash(contactField) {
this.updateForm.id = contactField.id;
this.persistClient(
'delete', 'people/' + this.hash + '/contactfield/' + contactField.id,
this.updateForm
);
if (this.contactInformationData.length <= 1) {
this.editMode = false;
}
},
persistClient(method, uri, form) {
form.errors = [];
axios[method](uri, form)
.then(response => {
this.getContactInformationData();
})
.catch(error => {
if (typeof error.response.data === 'object') {
form.errors = _.flatten(_.toArray(error.response.data));
} else {
form.errors = [this.$t('app.error_try_again')];
}
});
},
}
};
</script>

View File

@@ -0,0 +1,251 @@
<style>
table.vgt-table img.avatar-search {
border-radius: 3px;
display: inline-block;
height: 43px;
width: 43px;
}
</style>
<style lang="scss" scoped>
a {
color: inherit;
text-decoration: none;
vertical-align: middle;
background: transparent;
}
a:hover {
background: inherit;
color: inherit;
}
</style>
<template>
<div>
<vue-good-table
mode="remote"
:columns="columns"
:rows="contacts"
:total-rows="totalRecords"
:row-style-class="getRowStyleClass"
style-class="vgt-table"
:rtl="!dirltr"
:sort-options="{
enabled: false,
}"
:pagination-options="{
enabled: true,
position: 'bottom',
perPage: serverParams.perPage,
page: 1,
perPageDropdown: perPageDropdown,
mode: 'pages',
nextLabel: $t('people.people_search_next'),
prevLabel: $t('people.people_search_prev'),
rowsPerPageLabel: $t('people.people_search_rows_per_page'),
ofLabel: $t('people.people_search_of'),
pageLabel: $t('people.people_search_page'),
allLabel: $t('people.people_search_all'),
}"
:search-options="{
enabled: true,
placeholder: $t('people.people_search'),
}"
@on-page-change="onPageChange"
@on-per-page-change="onPerPageChange"
@on-search="onSearch"
@on-row-click="onRowClick"
>
<div slot="emptystate" class="tc">
{{ $t('people.people_search_no_results') }}
</div>
<template #emptystate>
<div v-if="!ready" class="vgt-center-align vgt-text-disabled h3">
{{ $t('app.loading') }}
</div>
<div v-else class="vgt-center-align vgt-text-disabled h3">
{{ $t('people.people_search_no_results') }}
</div>
</template>
<template slot="table-row" slot-scope="props">
<template v-if="props.column.field === 'avatar'">
<contact-item
:item="props.row"
:with-name="false"
:class-name="'avatar-search'"
/>
</template>
<template v-else-if="props.column.field === 'name'">
<a :href="props.row.route">
<template v-if="props.row.is_starred">
<span :class="[dirltr === 'ltr' ? 'ml3' : 'mr3']">
{{ props.row.complete_name }}
</span>
<svg class="relative" style="top: 5px" width="23" height="22" viewBox="0 0 23 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M14.6053 7.404L14.7224 7.68675L15.0275 7.7111C16.7206 7.84628 18.1486 7.92359 19.2895 7.98536C19.9026 8.01855 20.4327 8.04725 20.8765 8.07803C21.5288 8.12327 21.9886 8.17235 22.2913 8.24003C22.3371 8.25027 22.3765 8.26035 22.4102 8.27001C22.3896 8.29619 22.3651 8.32572 22.336 8.35877C22.1328 8.58945 21.7914 8.89714 21.2913 9.31475C20.9474 9.60184 20.5299 9.93955 20.0459 10.331C19.1625 11.0455 18.0577 11.9391 16.7762 13.0302L16.543 13.2288L16.614 13.5267C17.0045 15.1663 17.3689 16.5406 17.6601 17.6391C17.819 18.2381 17.956 18.7552 18.0638 19.1886C18.2209 19.8206 18.3149 20.2704 18.3428 20.5768C18.347 20.6222 18.3493 20.6616 18.3505 20.6957C18.3176 20.6838 18.2798 20.6688 18.2367 20.6502C17.9532 20.5277 17.5539 20.2981 17.0014 19.9522C16.6264 19.7175 16.1833 19.4306 15.671 19.099C14.7143 18.4797 13.5162 17.7042 12.0695 16.8199L11.8087 16.6604L11.5478 16.82C10.0753 17.7209 8.86032 18.5085 7.89223 19.136C7.3851 19.4648 6.94572 19.7496 6.57253 19.9838C6.01576 20.3332 5.61353 20.5656 5.32808 20.6899C5.28721 20.7077 5.25111 20.7222 5.21941 20.7339C5.22088 20.7009 5.22355 20.663 5.22783 20.6197C5.25839 20.3111 5.35605 19.8582 5.51781 19.2225C5.62627 18.7962 5.76269 18.2914 5.92018 17.7087C6.22053 16.5972 6.59748 15.2024 7.00309 13.5286L7.07553 13.2297L6.84141 13.0303C5.52399 11.9079 4.39683 10.9982 3.50024 10.2747C3.03915 9.90254 2.63904 9.57963 2.30539 9.30232C1.80195 8.88388 1.45729 8.57562 1.25116 8.34437C1.22315 8.31293 1.19929 8.28466 1.17903 8.25939C1.20999 8.25084 1.24557 8.24198 1.28628 8.233C1.58841 8.1663 2.048 8.11835 2.701 8.07418C3.1353 8.0448 3.65101 8.01744 4.24568 7.98589C5.39523 7.9249 6.83989 7.84824 8.56208 7.71111L8.86638 7.68688L8.98388 7.40514C9.61646 5.88824 10.1238 4.58366 10.5314 3.53571C10.7656 2.93365 10.9668 2.4163 11.1399 1.99205C11.3854 1.39027 11.5751 0.972355 11.7339 0.708729C11.7601 0.66516 11.7838 0.628777 11.8048 0.598565C11.8256 0.628571 11.849 0.664658 11.8748 0.707817C12.0327 0.971308 12.2212 1.38911 12.465 1.99089C12.6368 2.41509 12.8365 2.93242 13.0689 3.53445C13.4735 4.58244 13.9771 5.88709 14.6053 7.404Z"
fill="#F2C94C" stroke="#DCBB58"
/>
</svg>
</template>
<span v-else>
{{ props.row.complete_name }}
</span>
</a>
</template>
<template v-else-if="props.column.field === 'description'">
<a :href="props.row.route">
<span v-if="props.row.description" :class="['i', dirltr === 'ltr' ? 'ml3' : 'mr3']">
{{ props.row.description }}
</span>
</a>
</template>
</template>
</vue-good-table>
</div>
</template>
<script>
import ContactItem from './partials/ContactItem.vue';
export default {
components: {
ContactItem,
},
props: {
showArchived: {
type: Boolean,
default: false,
},
debounceWait: {
type: Number,
default: 200,
},
},
data() {
return {
contacts: [],
searchEntries: null,
ready: false,
totalRecords: 0,
perPageDropdown: [30, 50, 100],
serverParams: {
search: '',
page: 1,
perPage: 30
},
columns: [
{
label: this.$t('app.contact_list_avatar'),
field: 'avatar',
width: '70px',
sortable: false,
},
{
label: this.$t('app.contact_list_name'),
field: 'name',
},
{
label: this.$t('app.contact_list_description'),
field: 'description',
}
],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.searchEntries = _.debounce(() => {
this.ready = false;
this.contacts = [];
this._loadItems();
}, this.debounceWait);
this._loadItems();
},
methods: {
onRowClick(params) {
params.event.preventDefault();
if (params.event.ctrlKey) {
window.open(params.row.route, '_blank');
return;
}
window.location.href = params.row.route;
},
updateParams(newProps) {
this.serverParams = Object.assign({}, this.serverParams, newProps);
},
onPageChange(params) {
this.updateParams({page: params.currentPage});
this.searchEntries();
},
onPerPageChange(params) {
this.updateParams({perPage: params.currentPerPage, page: 1});
this.searchEntries();
},
onSearch(params) {
this.updateParams({search: params.searchTerm});
this.searchEntries();
},
// load items is what brings back the rows from server
_loadItems() {
let urlParam = window.location.search;
if (urlParam) {
urlParam += '&';
} else {
urlParam += '?';
}
urlParam += 'page='+this.serverParams.page;
urlParam += '&perPage='+this.serverParams.perPage;
urlParam += '&search='+this.serverParams.search;
if (this.showArchived) {
urlParam += '&show_archived=true';
}
this._loadNewItems(urlParam, (entries, total) => {
this.contacts = entries;
this.totalRecords = total;
this.ready = true;
});
},
_loadNewItems(urlParam, after) {
axios.get('people/list'+urlParam)
.then(response => {
if (_.isFunction(after)) {
after(
_.uniqBy(response.data.contacts, entry => _.uniqueId()),
response.data.totalRecords
);
}
});
},
getRowStyleClass() {
return 'people-list-item bg-white pointer';
}
}
};
</script>

View File

@@ -0,0 +1,106 @@
<style lang="scss" >
</style>
<template>
<div>
<contact-autosuggest
:id="id"
ref="contactauto"
:title="title"
:required="required"
:placeholder="placeholder"
:component-item="componentItem"
:filter="filter"
:add-no-result="false"
:input-class="'user-input-search-input'"
:overflow="true"
@select="select"
/>
<ul class="contacts mt2">
<ul class="contacts-list table">
<li v-for="contact in items" :key="contact.id" class="table-row">
<div class="table-cell w-80">
<strong>{{ contact.complete_name }}</strong>
</div>
<div class="table-cell actions">
<a class="pointer" href="" @click.prevent="remove(contact)">
{{ $t('app.delete') }}
</a>
</div>
<input type="hidden" name="contacts[]" :value="contact.id" />
</li>
</ul>
</ul>
</div>
</template>
<script>
import ContactAutosuggest from './partials/ContactAutosuggest.vue';
import ContactMultiItem from './partials/ContactMultiItem.vue';
export default {
components: {
ContactAutosuggest
},
props: {
id: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
required: {
type: Boolean,
default: true,
},
placeholder : {
type: String,
default: '',
},
userContactId: {
type: Number,
default: 0,
},
contacts: {
type: Array,
default: () => []
}
},
data() {
return {
items: [],
};
},
computed: {
componentItem() {
return ContactMultiItem;
}
},
mounted() {
this.items = this.contacts;
},
methods: {
filter(item) {
return _.findIndex(this.items, i => { return i.id === item.id; }) < 0;
},
select(contact) {
if (contact.item && contact.item.id > 0 && this.filter(contact.item)) {
this.items.push(contact.item);
}
},
remove(contact) {
this.items.splice(this.items.indexOf(contact), 1);
}
}
};
</script>

View File

@@ -0,0 +1,101 @@
<style lang="scss" >
</style>
<template>
<div>
<contact-autosuggest
:title="title"
:required="required"
:placeholder="placeholder"
:component-item="componentItem"
:input-class="'header-search-input'"
@select="select"
/>
</div>
</template>
<script>
import ContactAutosuggest from './partials/ContactAutosuggest.vue';
import ContactItem from './partials/ContactItem.vue';
export default {
components: {
ContactAutosuggest
},
props: {
title: {
type: String,
default: null,
},
required: {
type: Boolean,
default: true,
},
placeholder : {
type: String,
default: '',
},
formNameOrder : {
type: String,
default: 'firstname',
},
},
computed: {
componentItem() {
return ContactItem;
}
},
methods: {
select(contact) {
if (contact.item.id > 0) {
window.location = contact.item.route;
} else {
// contact with ID = -1 is the 'add person' contact
const keyword = contact.item.keyword.trim();
let names, email;
// attempt to extract name and email from 'first last <email@example.com>' format
// https://stackoverflow.com/questions/9558608/regex-for-parsing-name-and-email-from-a-single-string
const emailAndNameMatch = keyword.match(/(.*[^\s*<])?\s*<(.*)>/);
if(emailAndNameMatch === null) {
names = keyword;
} else {
names = emailAndNameMatch[1];
email = emailAndNameMatch[2];
}
names = names.split(' ').map(name => _.capitalize(name));
let first_name, last_name;
if (this.formNameOrder === 'firstname') {
first_name = names[0];
last_name = names.slice(1).join(' ');
} else {
first_name = names.slice(1).join(' ');
last_name = names[0];
}
const params = new URLSearchParams();
if (first_name) {
params.set('first_name', first_name);
}
if (last_name) {
params.set('last_name', last_name);
}
if (email) {
params.set('email', email);
}
let p = params.toString();
window.location = 'people/add' + (p !== '' ? '?' + p : '');
}
},
}
};
</script>

View File

@@ -0,0 +1,146 @@
<template>
<div>
<p v-if="title" class="mb2" :class="{ b: required }">
{{ title }}
</p>
<input type="hidden" :name="name" :value="selected ? selected.id : ''" />
<v-select
:id="id ? id : ''"
:value="selected"
:placeholder="placeholder"
:label="'complete_name'"
:options="items"
:dir="$root.htmldir"
@search="search"
@search:blur="blur"
@search:focus="focus"
@input="selected = $event; $emit('input', $event)"
/>
</div>
</template>
<script>
import vSelect from 'vue-select';
import 'vue-select/dist/vue-select.css';
import axios from 'axios';
export default {
components: {
vSelect
},
props: {
id: {
type: String,
default: null,
},
value: {
type: Object,
default: null,
},
name: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
required: {
type: Boolean,
default: true,
},
userContactId: {
type: Number,
default: null,
},
defaultOptions: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: '',
},
wait : {
type: Number,
default: 200,
},
},
data () {
return {
selected: null,
items: [],
callUpdateItems: null,
cache: [],
};
},
watch: {
value(newValue) {
this.selected = newValue;
}
},
mounted() {
this.selected = this.value;
this.callUpdateItems = _.debounce((text) => {
this.getContacts(text, this)
.then((response) => {
this.cache[text] = response;
this.displayItems(text);
});
}, this.wait);
this.items = this.defaultOptions;
},
methods: {
updateItems(text) {
if (text === null) {
return;
}
if (text.length < this.minLen) {
this.items = [];
return;
}
if (this.cache[text] === undefined) {
this.callUpdateItems(text);
} else {
this.callUpdateItems.cancel();
this.displayItems(text);
}
},
displayItems(text) {
var datas = [];
if (text === undefined || text.length === 0) {
datas = this.defaultOptions;
} else {
datas = this.cache[text];
}
datas = datas.filter(this.filter);
this.items = datas;
},
getContacts(keyword, vm) {
return axios.post('people/search', {
needle: keyword
}).then(function(response) {
return response.data.data;
});
},
filter(item) {
return this.userContactId !== item.id;
},
search(keyword, loading) {
this.updateItems(keyword);
},
}
};
</script>

View File

@@ -0,0 +1,193 @@
<style scoped>
.emotion-action-menu {
border-radius: 3px;
box-shadow: 1px 0px 1px rgba(43, 45, 80, 0.16), -1px 1px 1px rgba(43, 45, 80, 0.16), 0px 1px 4px rgba(43, 45, 80, 0.18);
top: 34px;
left: 0px;
width: 150px;
}
.emotion-action-menu li:last-child {
border-bottom: 0;
}
.emotion {
background: #E5F3F9;
border-radius: 7px;
}
.emotion span {
border-left-color: #A6C8D6;
}
.emotion-add-arrow {
right: 10px;
top: 12px;
}
.emotion-list-line:hover {
background-color: #f1f5fd;
}
</style>
<template>
<div>
<div class="relative">
<!-- CHOSEN EMOTIONS -->
<ul v-show="chosenEmotions.length !== 0" class="mr2 di">
<li v-for="chosenEmotion in chosenEmotions" :key="chosenEmotion.id" class="dib emotion br5 mr2">
<span class="ph2 pv1 dib">
{{ $t('app.emotion_' + chosenEmotion.name) }}
</span>
<span class="bl ph2 pv1 f6 pointer" @click.prevent="removeEmotion(chosenEmotion)">
</span>
</li>
</ul>
<div class="relative dib">
<a class="pointer small-btn pa2" @click.prevent="menu = true">
😐 {{ $t('people.emotion_this_made_me_feel') }}
</a>
<!-- MENU OF EMOTIONS -->
<ul v-show="menu" class="absolute emotion-action-menu bg-white z-max pv1">
<!-- PRIMARY -->
<li v-for="primaryEmotion in primaryEmotions" v-show="emotionsMenu === 'primary'" :key="'primary' + primaryEmotion.id" class="pa2 pointer relative emotion-list-line" @click.prevent="showSecondary(primaryEmotion)">
{{ $t('app.emotion_primary_' + primaryEmotion.name) }}
<svg class="absolute emotion-add-arrow" width="10" height="13" viewBox="0 0 10 13" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.75071 5.66783C9.34483 6.06361 9.34483 6.93653 8.75072 7.33231L1.80442 11.9598C1.13984 12.4025 0.25 11.9261 0.25 11.1275L0.25 1.87263C0.25 1.07409 1.13984 0.59767 1.80442 1.04039L8.75071 5.66783Z" fill="#C4C4C4" />
</svg>
</li>
<!-- SECONDARY -->
<li v-show="emotionsMenu === 'secondary'" class="pa2 pointer bb b--gray-monica">
<a class="no-underline" @click.prevent="emotionsMenu = 'primary'">
{{ $t('app.back') }}
</a>
</li>
<li v-for="secondaryEmotion in secondaryEmotions" v-show="emotionsMenu === 'secondary'" :key="'secondary' + secondaryEmotion.id" class="pa2 pointer relative emotion-list-line" @click.prevent="showEmotion(secondaryEmotion)">
{{ $t('app.emotion_secondary_' + secondaryEmotion.name) }}
<svg class="absolute emotion-add-arrow" width="10" height="13" viewBox="0 0 10 13" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.75071 5.66783C9.34483 6.06361 9.34483 6.93653 8.75072 7.33231L1.80442 11.9598C1.13984 12.4025 0.25 11.9261 0.25 11.1275L0.25 1.87263C0.25 1.07409 1.13984 0.59767 1.80442 1.04039L8.75071 5.66783Z" fill="#C4C4C4" />
</svg>
</li>
<!-- EMOTION -->
<li v-show="emotionsMenu === 'emotions'" class="pa2 pointer bb b--gray-monica">
<a class="no-underline" @click.prevent="emotionsMenu = 'secondary'">
{{ $t('app.back') }}
</a>
</li>
<li v-for="emotion in emotions" v-show="emotionsMenu === 'emotions'" :key="emotion.id" class="pa2 pointer emotion-list-line" @click.prevent="addEmotion(emotion)">
{{ $t('app.emotion_' + emotion.name) }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
initialEmotions: {
type: Array,
default: function () {
return [];
}
}
},
data() {
return {
emotions: [],
primaryEmotions: [],
secondaryEmotions: [],
selectedPrimaryEmotionId: 0,
selectedSecondaryEmotionId: 0,
chosenEmotions: [],
menu: false,
emotionsMenu: 'primary',
};
},
mounted() {
this.prepareComponent();
this.chosenEmotions = this.initialEmotions;
},
created() {
window.addEventListener('click', this.close);
},
beforeDestroy() {
window.removeEventListener('click', this.close);
},
methods: {
prepareComponent() {
this.getPrimaryEmotions();
},
close(e) {
if (!this.$el.contains(e.target)) {
this.menu = false;
}
},
getPrimaryEmotions() {
axios.get('emotions')
.then(response => {
this.primaryEmotions = response.data.data;
});
},
getSecondaryEmotions() {
axios.get('emotions/primaries/' + this.selectedPrimaryEmotionId + '/secondaries')
.then(response => {
this.secondaryEmotions = response.data.data;
});
},
getEmotions(id) {
axios.get('emotions/primaries/' + this.selectedPrimaryEmotionId + '/secondaries/' + this.selectedSecondaryEmotionId + '/emotions')
.then(response => {
this.emotions = response.data.data;
});
},
showSecondary(primaryEmotion) {
this.selectedPrimaryEmotionId = primaryEmotion.id;
this.getSecondaryEmotions();
this.emotionsMenu = 'secondary';
},
showEmotion(secondaryEmotion) {
this.selectedSecondaryEmotionId = secondaryEmotion.id;
this.getEmotions();
this.emotionsMenu = 'emotions';
},
addEmotion(emotion) {
this.menu = false;
this.chosenEmotions.push(emotion);
this.emotionsMenu = 'primary';
this.$emit('update', this.chosenEmotions);
},
removeEmotion(emotion) {
this.chosenEmotions.splice(emotion, 1);
this.$emit('update', this.chosenEmotions);
}
}
};
</script>

View File

@@ -0,0 +1,138 @@
<style scoped style="scss">
div >>> .avatar-small {
height: 30px;
width: 30px;
font-size: 13px;
border-radius: 0.5rem;
}
</style>
<template>
<div>
<div v-if="!limited" class="flex fr">
<a class="btn btn-primary" href="" @click.prevent="openModal">{{ $t('settings.me_select') }}</a>
</div>
<div v-if="meContact" class="dib pointer ml2 fl collapse">
<span class="dt-row">
<span class="dtc">
<avatar :contact="meContact" :clickable="true" :imgclass="'avatar-small br1'" />
</span>
<span class="dtc">
<a :href="'people/' + meContact.hash_id" class="avatar-small" :class="dirltr ? 'ml1' : 'mr1'">
{{ meContact.complete_name }}
</a>
</span>
</span>
</div>
<div v-else class="dib pointer fl">
{{ $t('settings.me_no_contact') }}<br />
<a v-if="!limited" href="" @click.prevent="openModal">{{ $t('settings.me_select_click') }}</a>
<div v-else v-html="$t('settings.personalisation_paid_upgrade_vue', {url: 'settings/subscriptions' })"></div>
</div>
<div class="cb"></div>
<sweet-modal ref="modal" overlay-theme="dark" :title="$t('settings.me_select')">
<form>
<contact-select
v-model="newContact"
:required="true"
:title="$t('settings.me_choose')"
:name="'me_contact_id'"
:placeholder="$t('settings.me_choose_placeholder')"
:default-options="existingContacts"
/>
</form>
<div slot="button">
<a class="btn fl" href="" @click.prevent="remove">
{{ $t('settings.me_remove_contact') }}
</a>
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="save">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal,
},
props: {
contact: {
type: Object,
default: null,
},
existingContacts: {
type: Array,
default: () => [],
},
limited: {
type: Boolean,
default: true,
},
},
data() {
return {
meContact: null,
newContact: null
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
watch: {
contact(value) {
this.newContact = value;
}
},
mounted() {
this.meContact = this.contact;
this.newContact = this.contact;
},
methods: {
save() {
axios.post('me/contact', {
contact_id: this.newContact.id
})
.then(response => {
this.$emit('change', this.newContact);
this.meContact = this.newContact;
this.closeModal();
});
},
remove() {
this.newContact = null;
axios.delete('me/contact')
.then(response => {
this.$emit('change', null);
this.meContact = null;
this.closeModal();
});
},
openModal() {
this.$refs.modal.open();
},
closeModal() {
this.$refs.modal.close();
}
}
};
</script>

View File

@@ -0,0 +1,229 @@
<style scoped>
.note:hover {
background-color: #f6fbff;
}
</style>
<template>
<div>
<notifications group="main" position="bottom right" />
<div>
<div>
<form class="bg-near-white pa2 br2 mb3">
<textarea v-model="newNote.body" v-cy-name="'add-note-textarea'" class="w-100 br2 pa2 b--light-gray" :placeholder="$t('people.notes_add_cta')" @focus="addMode = true"
@keyup.esc="addMode = false"
></textarea>
<a v-if="addMode" v-cy-name="'add-note-button'" class="pointer btn btn-primary" href="" @click.prevent="store">
{{ $t('app.add') }}
</a>
<a v-if="addMode" v-cy-name="'cancel-note-button'" class="pointer btn btn-secondary" href="" @click.prevent="addMode = false">
{{ $t('app.cancel') }}
</a>
</form>
</div>
<!-- LIST OF NORMAL NOTES -->
<ul v-cy-name="'notes-body'" v-cy-items="notes.map(n => n.id)">
<li v-for="note in notes" :key="note.id" class="note">
<div v-show="!note.edit" v-cy-name="'note-body-' + note.id" class="ba br2 b--black-10 br--top w-100 mb2">
<div class="pa2 markdown">
<span dir="auto" v-html="compiledMarkdown(note.body)"></span>
</div>
<div class="pa2 cf bt b--black-10 br--bottom f7 lh-copy">
<div class="fl w-50">
<div class="f5 di mr1">
<em v-tooltip.top="$t('people.notes_favorite')" class="pointer" :class="[note.is_favorited ? 'fa fa-star' : 'fa fa-star-o']" @click="toggleFavorite(note)"></em>
</div>
{{ note.created_at_short }}
</div>
<div class="fl w-50 tr">
<a v-cy-name="'edit-note-button-' + note.id" class="pointer" href="" @click.prevent="toggleEditMode(note)">
{{ $t('app.edit') }}
</a>
|
<a v-cy-name="'delete-note-button-' + note.id" class="pointer" href="" @click.prevent="showDelete(note)">
{{ $t('app.delete') }}
</a>
</div>
</div>
</div>
<!-- EDIT MODE -->
<form v-show="note.edit" class="bg-near-white pa2 br2 mt3 mb3">
<textarea v-model="note.body" v-cy-name="'edit-note-body-' + note.id" class="w-100 br2 pa2 b--light-gray" @keyup.esc="note.edit = false"></textarea>
<a v-cy-name="'edit-mode-note-button-' + note.id" class="pointer btn btn-primary" href="" @click.prevent="update(note)">
{{ $t('app.update') }}
</a>
</form>
</li>
</ul>
</div>
<!-- Delete Note modal -->
<sweet-modal ref="modalDeleteNote" v-cy-name="'modal-delete-note'"
overlay-theme="dark"
:title="$t('people.notes_delete_title')"
>
<p>
{{ $t('people.notes_delete_confirmation') }}
</p>
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'delete-mode-note-button-' + deleteNote.id" class="btn btn-primary" href=""
@click.prevent="trash(deleteNote)"
>
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal,
},
props: {
hash: {
type: String,
default: '',
},
},
data() {
return {
notes: [],
addMode: false,
editMode: false,
newNote: {
id: 0,
body: '',
is_favorited: 0
},
deleteNote: {
id: 0,
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getNotes();
},
reinitialize() {
this.createForm.body = '';
},
favorited: function (notes) {
return notes.filter(function (note) {
return note.is_favorited === true;
});
},
normal: function (notes) {
return notes.filter(function (note) {
return note.is_favorited === false;
});
},
toggleEditMode(note) {
Vue.set(note, 'edit', !note.edit);
},
getNotes() {
axios.get('people/' + this.hash + '/notes')
.then(response => {
this.notes = response.data;
});
},
store() {
axios.post('people/' + this.hash + '/notes', this.newNote)
.then(response => {
this.newNote.body = '';
this.getNotes();
this.addMode = false;
this.$notify({
group: 'main',
title: this.$t('people.notes_create_success'),
text: '',
type: 'success'
});
});
},
toggleFavorite(note) {
axios.post('people/' + this.hash + '/notes/' + note.id + '/toggle')
.then(response => {
this.getNotes();
});
},
update(note) {
axios.put('people/' + this.hash + '/notes/' + note.id, note)
.then(response => {
Vue.set(note, 'edit', false);
this.$notify({
group: 'main',
title: this.$t('people.notes_update_success'),
text: '',
type: 'success'
});
});
},
showDelete(note) {
this.deleteNote.id = note.id;
this.$refs.modalDeleteNote.open();
},
closeModal() {
this.$refs.modalDeleteNote.close();
},
trash(note) {
axios.delete('people/' + this.hash + '/notes/' + note.id)
.then(response => {
this.getNotes();
this.closeModal();
this.$notify({
group: 'main',
title: this.$t('people.notes_delete_success'),
text: '',
type: 'success'
});
});
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
},
}
};
</script>

View File

@@ -0,0 +1,128 @@
<style scoped>
.participant {
background: #E5F3F9;
border-radius: 7px;
}
.participant-list {
height: 150px;
}
input[type=text] {
background-color: #f5f5f5;
}
input[type=text]:focus {
background-color: #fff;
}
.potential-participant:hover {
background-color: #f1f5fd;
}
</style>
<template>
<div class="relative">
<ul v-show="chosenParticipants.length !== 0" class="mr2 mb3">
<li v-for="chosenParticipant in chosenParticipants"
:key="chosenParticipant.id"
class="dib participant br5 mr2"
>
<span class="ph2 pv1 dib">
{{ chosenParticipant.name }}
</span>
<span class="bl ph2 pv1 f6 pointer" @click.prevent="remove(chosenParticipant)">
</span>
</li>
</ul>
<div v-show="participants.length !== 0" class="ba b--gray-monica">
<span class="db bb b--gray-monica pa2">
<input v-model="search" type="text" :placeholder="$t('app.filter')" class="br2 f5 w-100 ba b--black-20 pa2 outline-0" />
</span>
<ul class="overflow-auto participant-list">
<li v-for="fparticipant in filteredList"
:key="fparticipant.id"
class="bb b--gray-monica pa2 pointer potential-participant"
@click.prevent="select(fparticipant)"
>
{{ fparticipant.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
initialParticipants: {
type: Array,
default: () => [],
},
hash: {
type: String,
default: '',
},
},
data() {
return {
search: '',
participants: [],
chosenParticipants: [],
};
},
computed: {
filteredList() {
// filter the list when searching
// also, sort the list by name
var list;
list = this.participants.filter(participant => {
return participant.name.toLowerCase().includes(this.search.toLowerCase())
&& this.chosenParticipants.find(p => p.id === participant.id) === undefined;
});
function compare(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}
return list.sort(compare);
}
},
mounted() {
this.prepareComponent();
this.chosenParticipants = this.initialParticipants;
},
methods: {
prepareComponent() {
this.getParticipants();
},
getParticipants: function () {
axios.get('people/' + this.hash + '/activities/contacts')
.then(response => {
this.participants = _.toArray(response.data);
});
},
select(participant) {
this.chosenParticipants.push(participant);
this.participants.splice(this.participants.indexOf(participant), 1);
this.$emit('update', this.chosenParticipants);
},
remove(participant) {
this.participants.push(participant);
this.chosenParticipants.splice(this.chosenParticipants.indexOf(participant), 1);
},
}
};
</script>

View File

@@ -0,0 +1,249 @@
<template>
<div class="sidebar-box" :class="[ editMode ? 'edit' : '' ]">
<notifications group="main" position="bottom right" />
<div class="w-100 dt">
<div class="sidebar-box-title">
<h3>{{ $t('people.pets_title') }}</h3>
</div>
<div v-if="pets.length > 0" class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<a v-if="!editMode" class="pointer" href="" @click.prevent="editMode = true">
{{ $t('app.edit') }}
</a>
<a v-else class="pointer" href="" @click.prevent="resetState">
{{ $t('app.done') }}
</a>
</div>
</div>
<!-- Add button when box is empty -->
<p v-if="pets.length === 0 && !addMode" class="mb0">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</p>
<!-- List of pets -->
<ul v-if="pets.length > 0">
<li v-for="(pet, i) in pets" :key="pet.id" class="mb2">
<div v-show="!pet.edit" class="w-100 dt">
<div class="dtc">
{{ $t('people.pets_' + pet.category_name) }}
<span v-if="pet.name">
- {{ pet.name }}
</span>
</div>
<div v-if="editMode" class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<em class="fa fa-pencil-square-o pointer pr2" @click="toggleEdit(pet)"></em>
<em class="fa fa-trash-o pointer" @click="trash(pet)"></em>
</div>
</div>
<!-- Pet edit form -->
<div v-show="pet.edit" class="w-100">
<form class="measure center">
<div class="mt3">
<label :for="'edit-category' + i" class="db fw6 lh-copy f6">
{{ $t('people.pets_kind') }}
</label>
<select :id="'edit-category' + i" v-model="updateForm.pet_category_id" class="db w-100 h2">
<option v-for="petCategory in petCategories" :key="petCategory.id" :value="petCategory.id">
{{ $t('people.pets_' + petCategory.name) }}
</option>
</select>
</div>
<div class="mt3">
<label :for="'edit-name' + i" class="db fw6 lh-copy f6">
{{ $t('people.pets_name') }}
</label>
<input :id="'edit-name' + i" v-model="updateForm.name" class="pa2 db w-100" type="text" @keyup.enter="update(pet)" />
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="update(pet)">
{{ $t('app.save') }}
</a>
<a class="btn" href="" @click.prevent="toggleEdit(pet)">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</li>
<li v-if="editMode && !addMode">
<a class="pointer" href="" @click.prevent="toggleAdd">
{{ $t('app.add') }}
</a>
</li>
</ul>
<!-- Pet Add form -->
<div v-if="addMode">
<form class="measure center">
<div class="mt3">
<label for="add-category" class="db fw6 lh-copy f6">
{{ $t('people.pets_kind') }}
</label>
<select id="add-category" v-model="createForm.pet_category_id" class="db w-100 h2">
<option v-for="petCategory in petCategories" :key="petCategory.id" :value="petCategory.id">
{{ $t('people.pets_' + petCategory.name) }}
</option>
</select>
</div>
<div class="mt3">
<label for="add-name" class="db fw6 lh-copy f6">
{{ $t('people.pets_name') }}
</label>
<input id="add-name" v-model="createForm.name" class="pa2 db w-100" type="text" @keyup.enter="store"
@keyup.esc="resetState"
/>
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.add') }}
</a>
<a class="btn" href="" @click.prevent="resetState">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
},
data() {
return {
petCategories: [],
pets: [],
editMode: false,
addMode: false,
updateMode: false,
createForm: {
pet_category_id: '',
name: '',
errors: []
},
updateForm: {
id: '',
pet_category_id: '',
name: '',
edit: false,
errors: []
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getPetCategories();
this.getPets();
},
getPetCategories() {
axios.get('petcategories')
.then(response => {
this.petCategories = response.data;
});
},
getPets() {
axios.get('people/' + this.hash + '/pets')
.then(response => {
this.pets = response.data;
});
},
store() {
axios.post('people/' + this.hash + '/pets', this.createForm)
.then(response => {
this.addMode = false;
this.pets.push(response.data);
this.createForm.name = '';
this.$notify({
group: 'main',
title: this.$t('people.pets_create_success'),
text: '',
type: 'success'
});
});
},
resetState() {
this.editMode = false;
this.addMode = false;
},
toggleAdd() {
this.addMode = true;
this.editMode = true;
this.createForm.data = '';
this.createForm.pet_category_id = '';
},
toggleEdit(pet) {
Vue.set(pet, 'edit', !pet.edit);
this.updateForm.id = pet.id;
this.updateForm.name = pet.name;
this.updateForm.pet_category_id = pet.pet_category_id;
},
update(pet) {
axios.put('people/' + this.hash + '/pets/' + pet.id, this.updateForm)
.then(response => {
Vue.set(pet, 'edit', !pet.edit);
Vue.set(pet, 'name', response.data.name);
Vue.set(pet, 'pet_category_id', response.data.pet_category_id);
Vue.set(pet, 'category_name', response.data.category_name);
this.$notify({
group: 'main',
title: this.$t('people.pets_update_success'),
text: '',
type: 'success'
});
});
},
trash(pet) {
axios.delete('people/' + this.hash + '/pets/' + pet.id)
.then(response => {
this.getPets();
this.$notify({
group: 'main',
title: this.$t('people.pets_delete_success'),
text: '',
type: 'success'
});
});
if (this.pets.length <= 1) {
this.resetState();
}
},
}
};
</script>

View File

@@ -0,0 +1,219 @@
<template>
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<p>{{ $t('people.avatar_question') }}</p>
<div class="mb3 mb0-ns">
<!-- Default avatar -->
<form-radio
v-model="selectedAvatar"
:name="'avatar'"
:value="'default'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_default_avatar') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="defaultUrl" alt="" />
</div>
</form-radio>
<!-- Adorable avatar -->
<form-radio
v-model="selectedAvatar"
:name="'avatar'"
:value="'adorable'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_adorable_avatar') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="adorableUrl" alt="" />
</div>
</form-radio>
<!-- Gravatar -->
<form-radio
v-if="gravatarUrl"
v-model="selectedAvatar"
:name="'avatar'"
:value="'gravatar'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
<span v-html="$t('people.avatar_gravatar')"></span>
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="gravatarUrl" alt="" />
</div>
</form-radio>
<!-- Existing avatar -->
<form-radio
v-if="initialAvatar === 'photo'"
v-model="selectedAvatar"
:name="'avatar'"
:value="'photo'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
>
<template slot="label">
{{ $t('people.avatar_current') }}
</template>
<div slot="extra">
<img class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="photoUrl" alt="" />
</div>
</form-radio>
<!-- Upload avatar -->
<form-radio
v-model="selectedAvatar"
:name="'avatar'"
:value="'upload'"
:dclass="'flex mb1'"
:iclass="dirltr ? 'mr2' : 'ml2'"
:disabled="hasReachedAccountStorageLimit"
>
<template slot="label">
{{ $t('people.avatar_photo') }}
<span v-if="hasReachedAccountStorageLimit">
<a href="settings/subscriptions">
{{ $t('app.upgrade') }}
</a>
</span>
</template>
<div slot="extra">
<input ref="uploadedImg"
type="file"
class="form-control-file"
name="photo"
:disabled="hasReachedAccountStorageLimit"
@change="uploadImg($event)"
/>
<small class="form-text text-muted">
{{ $t('people.information_edit_max_size2', { size: maxUploadSize }) }}
</small>
<img v-if="croppedImgUrl" class="mb4 pa2 ba b--gray-monica br3" style="width: 150px" :src="croppedImgUrl" alt="" />
</div>
</form-radio>
</div>
<sweet-modal ref="cropModal" :title="$t('people.avatar_crop_new_avatar_photo')" :blocking="true" :hide-close-button="true">
<clipper-basic ref="clipper" :src="uploadedImgUrl" :ratio="1" :init-width="100" :init-height="100" />
<div slot="button">
<a class="btn" href="" @click.prevent="cancelCrop">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="setCroppedImg">
{{ $t('app.done') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { clipperBasic } from 'vuejs-clipper';
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
clipperBasic,
SweetModal
},
props: {
avatar: {
type: String,
default: '',
},
defaultUrl: {
type: String,
default: '',
},
adorableUrl: {
type: String,
default: '',
},
gravatarUrl: {
type: String,
default: '',
},
photoUrl: {
type: String,
default: '',
},
hasReachedAccountStorageLimit: {
type: Boolean,
default: false,
},
maxUploadSize: {
type: Number,
default: 10000,
},
},
data() {
return {
selectedAvatar: '',
initialAvatar: '',
uploadedImgUrl: '',
croppedImgUrl: '',
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
watch: {
avatar(val) {
this.selectedAvatar = val;
}
},
mounted() {
this.initialAvatar = this.avatar;
this.selectedAvatar = this.avatar;
},
methods: {
uploadImg: function(e) {
if (e.target.files.length !== 0) {
if(this.uploadedImgUrl) {
URL.revokeObjectURL(this.uploadedImgUrl);
}
this.uploadedImgUrl = window.URL.createObjectURL(e.target.files[0]);
this.$refs.cropModal.open();
}
},
setCroppedImg: function () {
const canvas = this.$refs.clipper.clip();
canvas.toBlob((blob) => {
const input = this.$refs.uploadedImg;
const file = new File([blob], input.files[0].name, { type: 'image/jpeg' });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files;
this.croppedImgUrl = window.URL.createObjectURL(blob);
}, 'image/jpeg', 1);
this.$refs.cropModal.close();
},
cancelCrop() {
const dataTransfer = new DataTransfer();
this.$refs.uploadedImg.files = dataTransfer.files;
this.croppedImgUrl = '';
this.$refs.cropModal.close();
},
},
};
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="di f5 normal">
<notifications group="favorite" position="top middle" width="400" />
<svg v-if="!isFavorite" v-tooltip.top="$t('people.set_favorite')" v-cy-name="'set-favorite'" class="pointer" width="23"
height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg" @click="store(true)"
>
<path d="M14.6053 7.404L14.7224 7.68675L15.0275 7.7111C16.7206 7.84628 18.1486 7.92359 19.2895 7.98536C19.9026 8.01855 20.4327 8.04725 20.8765 8.07803C21.5288 8.12327 21.9886 8.17235 22.2913 8.24003C22.3371 8.25027 22.3765 8.26035 22.4102 8.27001C22.3896 8.29619 22.3651 8.32572 22.336 8.35877C22.1328 8.58945 21.7914 8.89714 21.2913 9.31475C20.9474 9.60184 20.5299 9.93955 20.0459 10.331C19.1625 11.0455 18.0577 11.9391 16.7762 13.0302L16.543 13.2288L16.614 13.5267C17.0045 15.1663 17.3689 16.5406 17.6601 17.6391C17.819 18.2381 17.956 18.7552 18.0638 19.1886C18.2209 19.8206 18.3149 20.2704 18.3428 20.5768C18.347 20.6222 18.3493 20.6616 18.3505 20.6957C18.3176 20.6838 18.2798 20.6688 18.2367 20.6502C17.9532 20.5277 17.5539 20.2981 17.0014 19.9522C16.6264 19.7175 16.1833 19.4306 15.671 19.099C14.7143 18.4797 13.5162 17.7042 12.0695 16.8199L11.8087 16.6604L11.5478 16.82C10.0753 17.7209 8.86032 18.5085 7.89223 19.136C7.3851 19.4648 6.94572 19.7496 6.57253 19.9838C6.01576 20.3332 5.61353 20.5656 5.32808 20.6899C5.28721 20.7077 5.25111 20.7222 5.21941 20.7339C5.22088 20.7009 5.22355 20.663 5.22783 20.6197C5.25839 20.3111 5.35605 19.8582 5.51781 19.2225C5.62627 18.7962 5.76269 18.2914 5.92018 17.7087C6.22053 16.5972 6.59748 15.2024 7.00309 13.5286L7.07553 13.2297L6.84141 13.0303C5.52399 11.9079 4.39683 10.9982 3.50024 10.2747C3.03915 9.90254 2.63904 9.57963 2.30539 9.30232C1.80195 8.88388 1.45729 8.57562 1.25116 8.34437C1.22315 8.31293 1.19929 8.28466 1.17903 8.25939C1.20999 8.25084 1.24557 8.24198 1.28628 8.233C1.58841 8.1663 2.048 8.11835 2.701 8.07418C3.1353 8.0448 3.65101 8.01744 4.24568 7.98589C5.39523 7.9249 6.83989 7.84824 8.56208 7.71111L8.86638 7.68688L8.98388 7.40514C9.61646 5.88824 10.1238 4.58366 10.5314 3.53571C10.7656 2.93365 10.9668 2.4163 11.1399 1.99205C11.3854 1.39027 11.5751 0.972355 11.7339 0.708729C11.7601 0.66516 11.7838 0.628777 11.8048 0.598565C11.8256 0.628571 11.849 0.664658 11.8748 0.707817C12.0327 0.971308 12.2212 1.38911 12.465 1.99089C12.6368 2.41509 12.8365 2.93242 13.0689 3.53445C13.4735 4.58244 13.9771 5.88709 14.6053 7.404Z" fill="#D9D8D8" fill-opacity="0.25" stroke="#C6C6C6" />
</svg>
<svg v-else v-cy-name="'unset-favorite'" class="pointer" width="23" height="22"
viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg" @click="store(false)"
>
<path d="M14.6053 7.404L14.7224 7.68675L15.0275 7.7111C16.7206 7.84628 18.1486 7.92359 19.2895 7.98536C19.9026 8.01855 20.4327 8.04725 20.8765 8.07803C21.5288 8.12327 21.9886 8.17235 22.2913 8.24003C22.3371 8.25027 22.3765 8.26035 22.4102 8.27001C22.3896 8.29619 22.3651 8.32572 22.336 8.35877C22.1328 8.58945 21.7914 8.89714 21.2913 9.31475C20.9474 9.60184 20.5299 9.93955 20.0459 10.331C19.1625 11.0455 18.0577 11.9391 16.7762 13.0302L16.543 13.2288L16.614 13.5267C17.0045 15.1663 17.3689 16.5406 17.6601 17.6391C17.819 18.2381 17.956 18.7552 18.0638 19.1886C18.2209 19.8206 18.3149 20.2704 18.3428 20.5768C18.347 20.6222 18.3493 20.6616 18.3505 20.6957C18.3176 20.6838 18.2798 20.6688 18.2367 20.6502C17.9532 20.5277 17.5539 20.2981 17.0014 19.9522C16.6264 19.7175 16.1833 19.4306 15.671 19.099C14.7143 18.4797 13.5162 17.7042 12.0695 16.8199L11.8087 16.6604L11.5478 16.82C10.0753 17.7209 8.86032 18.5085 7.89223 19.136C7.3851 19.4648 6.94572 19.7496 6.57253 19.9838C6.01576 20.3332 5.61353 20.5656 5.32808 20.6899C5.28721 20.7077 5.25111 20.7222 5.21941 20.7339C5.22088 20.7009 5.22355 20.663 5.22783 20.6197C5.25839 20.3111 5.35605 19.8582 5.51781 19.2225C5.62627 18.7962 5.76269 18.2914 5.92018 17.7087C6.22053 16.5972 6.59748 15.2024 7.00309 13.5286L7.07553 13.2297L6.84141 13.0303C5.52399 11.9079 4.39683 10.9982 3.50024 10.2747C3.03915 9.90254 2.63904 9.57963 2.30539 9.30232C1.80195 8.88388 1.45729 8.57562 1.25116 8.34437C1.22315 8.31293 1.19929 8.28466 1.17903 8.25939C1.20999 8.25084 1.24557 8.24198 1.28628 8.233C1.58841 8.1663 2.048 8.11835 2.701 8.07418C3.1353 8.0448 3.65101 8.01744 4.24568 7.98589C5.39523 7.9249 6.83989 7.84824 8.56208 7.71111L8.86638 7.68688L8.98388 7.40514C9.61646 5.88824 10.1238 4.58366 10.5314 3.53571C10.7656 2.93365 10.9668 2.4163 11.1399 1.99205C11.3854 1.39027 11.5751 0.972355 11.7339 0.708729C11.7601 0.66516 11.7838 0.628777 11.8048 0.598565C11.8256 0.628571 11.849 0.664658 11.8748 0.707817C12.0327 0.971308 12.2212 1.38911 12.465 1.99089C12.6368 2.41509 12.8365 2.93242 13.0689 3.53445C13.4735 4.58244 13.9771 5.88709 14.6053 7.404Z" fill="#F2C94C" stroke="#DCBB58" />
</svg>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
starred: {
type: Boolean,
default: false,
},
},
data() {
return {
isFavorite: false,
};
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.isFavorite = this.starred;
},
store(toggle) {
axios.post('people/' + this.hash + '/favorite', {'toggle': toggle})
.then(response => {
this.isFavorite = response.data.is_starred;
this.$notify({
group: 'favorite',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
}
};
</script>

View File

@@ -0,0 +1,304 @@
<style scoped>
.error {
animation-name: shakeError;
animation-fill-mode: forwards;
animation-duration: .6s;
animation-timing-function: ease-in-out;
}
.form-group-error {
display: block;
color:#f57f6c;
}
.dashed {
border-bottom-style: dashed;
}
</style>
<template>
<div class="di">
<notifications group="main" position="bottom right" />
<!-- Contact doesn't have a frequency set -->
<a v-if="!isActive" class="pointer" href="" @click.prevent="showUpdate">
{{ $t('people.stay_in_touch_modal_title') }}
</a>
<!-- Contact has a frequency set -->
<div v-else class="di">
<span v-tooltip.bottom="$t('people.stay_in_touch_next_date', { date: formatDate(nextTriggerDate) })" class="bb dashed dib pointer nowrap-link">
{{ $tc('people.stay_in_touch_frequency', frequencyInput, { count: frequencyInput }) }}
</span>
<a class="pointer" href="" @click.prevent="showUpdate">
{{ $t('app.edit') }}
</a>
</div>
<sweet-modal ref="updateModal" overlay-theme="dark" :title="$t('people.stay_in_touch_modal_title')">
<div class="tc mw-100">
<svg viewBox="0 0 423 74" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs />
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-6" transform="translate(2.000000, 2.000000)">
<g id="Group-3" transform="translate(341.519872, 38.183805) rotate(17.000000) translate(-341.519872, -38.183805) translate(324.519872, 20.183805)">
<path id="Fill-45" d="M6.93842857,23.5330169 L2.60464286,22.0075932 C0.662392857,21.3235932 0.394035714,18.6742373 2.15839286,17.6107119 L29.2497143,1.28379661 C31.0863214,0.176949153 33.3461071,1.82745763 32.86525,3.92461017 L27.3171786,28.1343051 C26.9868929,29.5761356 25.466,30.3931525 24.0896071,29.8684068 L13.4160357,25.8010169 L23.88925,10.3942373 L6.93842857,23.5330169" fill="#BBE2EE" />
<path id="Fill-46" d="M6.93842857,23.5330169 L7.13939286,22.9576271 L2.80560714,21.4315932 C2.39639286,21.2863729 2.10132143,21.0404746 1.89671429,20.7353898 C1.69271429,20.4309153 1.58585714,20.0642034 1.58585714,19.6968814 C1.59071429,19.087322 1.86332143,18.5033898 2.47107143,18.1342373 L29.5617857,1.80671186 C29.8762857,1.61816949 30.1895714,1.53945763 30.4961786,1.53884746 C30.96975,1.53823729 31.4323929,1.73715254 31.7723929,2.06725424 C32.1117857,2.39918644 32.3230714,2.84522034 32.3236786,3.35471186 C32.3236786,3.49322034 32.3078929,3.63783051 32.2732857,3.7879322 L26.7258214,27.9970169 C26.5303214,28.8555254 25.7695714,29.4168814 24.9505357,29.4181017 C24.7374286,29.4181017 24.5206786,29.3802712 24.3045357,29.2978983 L14.35225,25.5050847 L24.3901429,10.7383729 C24.5589286,10.4906441 24.5231071,10.1562712 24.30575,9.9500339 C24.0883929,9.74440678 23.7544643,9.72732203 23.5182857,9.91098305 L6.56807143,23.0497627 L6.93842857,23.5330169 L7.13939286,22.9576271 L6.93842857,23.5330169 L7.30939286,24.0162712 L21.2275357,13.2278644 L12.9145357,25.4568814 C12.8095,25.6112542 12.7803571,25.804678 12.8368214,25.9834576 C12.8926786,26.1622373 13.02625,26.3050169 13.2005,26.3715254 L23.8740714,30.4389153 C24.2280357,30.5737627 24.5935357,30.6384407 24.9505357,30.6384407 C26.3190357,30.6390508 27.5800714,29.7 27.9091429,28.2709831 L33.4566071,4.06128814 C33.51125,3.82271186 33.5379643,3.5859661 33.5379643,3.35471186 C33.5385714,2.49559322 33.1730714,1.72922034 32.6163214,1.18983051 C32.0595714,0.649220339 31.3048929,0.319728814 30.4961786,0.318508475 C29.9758571,0.317898305 29.4355,0.459457627 28.9370357,0.760271186 L1.84632143,17.0871864 C0.8585,17.679661 0.366714286,18.7047458 0.371571429,19.6968814 C0.371571429,20.300339 0.545214286,20.9013559 0.88825,21.4157288 C1.23067857,21.9301017 1.74857143,22.3535593 2.40367857,22.5829831 L6.73807143,24.1090169 C6.93114286,24.1773559 7.14728571,24.1419661 7.30939286,24.0162712 L6.93842857,23.5330169" fill="#5CBBDE" />
<polyline id="Fill-47" fill="#BBE2EE" points="12.2703571 34.9425763 13.4160357 25.8010169 23.88925 10.3942373 6.93842857 23.5330169 12.2703571 34.9425763" />
<path id="Fill-48" d="M12.2703571,34.9425763 L12.8726429,35.0188475 L14.0001071,26.0231186 L24.3901429,10.7383729 C24.5589286,10.4906441 24.5231071,10.1562712 24.30575,9.9500339 C24.0883929,9.74440678 23.7544643,9.72732203 23.5182857,9.91098305 L6.56807143,23.0497627 C6.34282143,23.2242712 6.26814286,23.5342373 6.38896429,23.792339 L11.7202857,35.2018983 C11.8350357,35.4471864 12.0985357,35.5875254 12.3650714,35.5448136 C12.631,35.5027119 12.8386429,35.287322 12.8726429,35.0188475 L12.2703571,34.9425763 L12.8198214,34.6832542 L7.69553571,23.7172881 L21.2275357,13.2278644 L12.9145357,25.4568814 C12.8605,25.5362034 12.8252857,25.6289492 12.8131429,25.7247458 L11.6674643,34.8663051 L12.2703571,34.9425763 L12.8198214,34.6832542 L12.2703571,34.9425763" fill="#5CBBDE" />
<polyline id="Fill-49" fill="#BBE2EE" points="12.2703571 34.9425763 18.1013571 27.5863729 13.4160357 25.8010169 12.2703571 34.9425763" />
<path id="Fill-50" d="M12.2703571,34.9425763 L12.7451429,35.3227119 L18.5761429,27.9665085 C18.6975714,27.8133559 18.73825,27.6138305 18.6866429,27.424678 C18.6356429,27.2361356 18.4990357,27.0854237 18.3168929,27.0158644 L13.6309643,25.2305085 C13.4567143,25.164 13.2624286,25.1810847 13.10275,25.2781017 C12.9430714,25.3745085 12.8368214,25.5386441 12.8131429,25.7247458 L11.6674643,34.8663051 C11.6334643,35.1366102 11.7840357,35.3989831 12.0341786,35.5045424 C12.2843214,35.6107119 12.57575,35.535661 12.7451429,35.3227119 L12.2703571,34.9425763 L12.8726429,35.0188475 L13.9217857,26.6461017 L17.1080714,27.860339 L11.7949643,34.5624407 L12.2703571,34.9425763 L12.8726429,35.0188475 L12.2703571,34.9425763" fill="#5CBBDE" />
</g>
<g id="Group-2" transform="translate(275.500000, 38.000000) rotate(-4.000000) translate(-275.500000, -38.000000) translate(256.000000, 20.000000)">
<path id="Fill-97" d="M15.5451562,17.9652 C15.5451562,16.9386 16.3921875,16.1064 17.4342188,16.1064 L30.1275,16.1064 L30.1275,3.1872 C30.1275,1.8618 29.0367188,0.7872 27.69,0.7872 L5.143125,0.7872 C3.79640625,0.7872 2.705625,1.8618 2.705625,3.1872 L2.705625,22.3872 L1.22484375,27.4848 C0.90796875,28.5768 2.169375,29.451 3.1078125,28.791 L8.799375,24.7872 L15.5451562,24.7872 L15.5451562,17.9652" fill="#F5C894" />
<path id="Fill-98" d="M15.5451562,17.9652 L16.1545312,17.9652 C16.1545312,17.2698 16.7273438,16.7076 17.4342188,16.7058 L30.1275,16.7058 C30.2859375,16.7058 30.444375,16.6422 30.5601563,16.5306 C30.6698437,16.4184 30.736875,16.2642 30.736875,16.1064 L30.736875,3.1872 C30.736875,1.53 29.371875,0.1878 27.69,0.1872 L5.143125,0.1872 C3.46125,0.1878 2.09625,1.53 2.09625,3.1872 L2.09625,22.3032 L0.63984375,27.3198 C0.59109375,27.4896 0.56671875,27.6606 0.56671875,27.828 C0.56671875,28.338 0.7921875,28.794 1.12734375,29.1084 C1.4625,29.4246 1.9134375,29.6184 2.4009375,29.6196 C2.76046875,29.6202 3.13828125,29.5092 3.46125,29.2794 L8.994375,25.3872 L15.5451562,25.3872 C15.7035937,25.3872 15.8620312,25.323 15.9717188,25.2114 C16.0875,25.0998 16.1545312,24.945 16.1545312,24.7872 L16.1545312,17.9652 L14.9357812,17.9652 L14.9357812,24.1872 L8.799375,24.1872 C8.67140625,24.1872 8.54953125,24.2262 8.4459375,24.2988 L2.754375,28.3026 C2.62640625,28.3896 2.51671875,28.4184 2.4009375,28.4196 C2.24859375,28.4202 2.09015625,28.3542 1.974375,28.2432 C1.8525,28.1304 1.78546875,27.99 1.78546875,27.828 C1.78546875,27.774 1.7915625,27.7152 1.8159375,27.6498 L3.290625,22.5516 C3.30890625,22.4976 3.315,22.443 3.315,22.3872 L3.315,3.1872 C3.32109375,2.1936 4.13765625,1.389 5.143125,1.3872 L27.69,1.3872 C28.7015625,1.389 29.518125,2.1936 29.518125,3.1872 L29.518125,15.5058 L17.4342188,15.5064 C16.0509375,15.5064 14.9357812,16.6068 14.9357812,17.9652 L15.5451562,17.9652" fill="#E5742B" />
<path id="Fill-99" d="M34.9842187,16.1064 L17.4342188,16.1064 C16.3921875,16.1064 15.5451562,16.9386 15.5451562,17.9652 L15.5451562,31.047 C15.5451562,32.0736 16.3921875,32.9058 17.4342188,32.9058 L32.6076562,32.9058 L36.001875,34.9116 C36.9585937,35.4756 38.1164062,34.5876 37.7934375,33.5382 L36.8732812,30.5058 L36.8732812,17.9652 C36.8732812,16.9386 36.02625,16.1064 34.9842187,16.1064" fill="#BBE2EE" />
<path id="Fill-100" d="M34.9842188,16.1064 L34.9842188,15.5064 L17.4342188,15.5064 C16.0509375,15.5064 14.9357812,16.6068 14.9357812,17.9652 L14.9357812,31.047 C14.9357812,32.4054 16.0509375,33.5058 17.4342188,33.5058 L32.4370313,33.5058 L35.685,35.4264 C35.9835937,35.6022 36.3126562,35.6874 36.6295313,35.6868 C37.123125,35.6862 37.5740625,35.4888 37.903125,35.1708 C38.2382813,34.8534 38.4576563,34.3998 38.4576563,33.894 C38.4576563,33.7206 38.4332812,33.5424 38.3784375,33.366 L37.4826563,30.417 L37.4826563,17.9652 C37.4826563,16.6068 36.3614063,15.5064 34.9842188,15.5064 L34.9842188,16.7058 C35.6910937,16.7076 36.2639063,17.2698 36.2639063,17.9652 L36.2639063,30.5058 C36.2639063,30.5622 36.27,30.624 36.2882812,30.678 L37.2145312,33.7104 C37.2328125,33.7764 37.2389063,33.837 37.2389063,33.894 C37.2389063,34.0542 37.171875,34.197 37.0560937,34.3098 C36.9403125,34.422 36.781875,34.488 36.6295313,34.4868 C36.5259375,34.4862 36.4284375,34.4634 36.3126562,34.3974 L32.9184375,32.3916 C32.8270313,32.3358 32.7173438,32.3058 32.6076563,32.3058 L17.4342188,32.3058 C16.7273438,32.3046 16.1545313,31.7418 16.1545313,31.047 L16.1545313,17.9652 C16.1545313,17.2698 16.7273438,16.7076 17.4342188,16.7058 L34.9842188,16.7058 L34.9842188,16.1064" fill="#5CBBDE" />
<path id="Fill-101" d="M22.1446875,24.3966 C22.1446875,25.059 21.59625,25.5966 20.9259375,25.5966 C20.2495313,25.5966 19.7071875,25.059 19.7071875,24.3966 C19.7071875,23.7336 20.2495313,23.1966 20.9259375,23.1966 C21.59625,23.1966 22.1446875,23.7336 22.1446875,24.3966" fill="#FFFFFE" />
<path id="Fill-102" d="M27.6290625,24.3966 C27.6290625,25.059 27.080625,25.5966 26.4103125,25.5966 C25.7339063,25.5966 25.1915625,25.059 25.1915625,24.3966 C25.1915625,23.7336 25.7339063,23.1966 26.4103125,23.1966 C27.080625,23.1966 27.6290625,23.7336 27.6290625,24.3966" fill="#FFFFFE" />
<path id="Fill-103" d="M33.1134375,24.3966 C33.1134375,25.059 32.565,25.5966 31.8946875,25.5966 C31.2182813,25.5966 30.6759375,25.059 30.6759375,24.3966 C30.6759375,23.7336 31.2182813,23.1966 31.8946875,23.1966 C32.565,23.1966 33.1134375,23.7336 33.1134375,24.3966" fill="#FFFFFE" />
</g>
<g id="Group" transform="translate(120.210360, 37.580603) rotate(-14.000000) translate(-120.210360, -37.580603) translate(107.210360, 18.580603)">
<path id="Fill-130" d="M22.6586977,37.0813651 L3.30986047,37.0813651 C1.97418605,37.0813651 0.891255814,36.0010794 0.891255814,34.6686667 L0.891255814,3.3035873 C0.891255814,1.9711746 1.97418605,0.890888889 3.30986047,0.890888889 L22.6586977,0.890888889 C23.9943721,0.890888889 25.0773023,1.9711746 25.0773023,3.3035873 L25.0773023,34.6686667 C25.0773023,36.0010794 23.9943721,37.0813651 22.6586977,37.0813651" fill="#FCE499" />
<path id="Fill-131" d="M22.6586977,37.0813651 L22.6586977,36.4781905 L3.30986047,36.4781905 C2.30795349,36.476381 1.49772093,35.6675238 1.49590698,34.6686667 L1.49590698,3.3035873 C1.49772093,2.30412698 2.30795349,1.49587302 3.30986047,1.49406349 L22.6586977,1.49406349 C23.66,1.49587302 24.4708372,2.30412698 24.4726512,3.3035873 L24.4726512,34.6686667 C24.4708372,35.6675238 23.66,36.476381 22.6586977,36.4781905 L22.6586977,37.6845397 C24.3287442,37.6839365 25.6813488,36.3340317 25.6819535,34.6686667 L25.6819535,3.3035873 C25.6813488,1.63761905 24.3287442,0.287714286 22.6586977,0.287714286 L3.30986047,0.287714286 C1.63981395,0.287714286 0.287209302,1.63761905 0.286604651,3.3035873 L0.286604651,34.6686667 C0.287209302,36.3340317 1.63981395,37.6839365 3.30986047,37.6845397 L22.6586977,37.6845397 L22.6586977,37.0813651" fill="#F5BB27" />
<path id="Fill-132" d="M5.72846512,26.223619 L5.72846512,6.92203175 C5.72846512,6.25612698 6.26962791,5.71568254 6.93776744,5.71568254 L19.0307907,5.71568254 C19.6989302,5.71568254 20.240093,6.25612698 20.240093,6.92203175 L20.240093,26.223619 C20.240093,26.890127 19.6989302,27.4299683 19.0307907,27.4299683 L6.93776744,27.4299683 C6.26962791,27.4299683 5.72846512,26.890127 5.72846512,26.223619" fill="#BBE2EE" />
<path id="Fill-133" d="M5.72846512,26.223619 L6.33311628,26.223619 L6.33311628,6.92203175 C6.33372093,6.58907937 6.604,6.31946032 6.93776744,6.31885714 L19.0307907,6.31885714 C19.3645581,6.31946032 19.6348372,6.58907937 19.6354419,6.92203175 L19.6354419,26.223619 C19.6348372,26.5565714 19.3645581,26.8261905 19.0307907,26.8267937 L6.93776744,26.8267937 C6.604,26.8261905 6.33372093,26.5565714 6.33311628,26.223619 L5.12381395,26.223619 C5.1244186,27.2230794 5.93586047,28.0331429 6.93776744,28.0331429 L19.0307907,28.0331429 C20.0326977,28.0331429 20.8441395,27.2230794 20.8447442,26.223619 L20.8447442,6.92203175 C20.8441395,5.92257143 20.0326977,5.11311111 19.0307907,5.11250794 L6.93776744,5.11250794 C5.93586047,5.11311111 5.1244186,5.92257143 5.12381395,6.92203175 L5.12381395,26.223619 L5.72846512,26.223619" fill="#5CBBDE" />
<path id="Fill-134" d="M16.0075349,33.3615873 L9.96102326,33.3615873 C9.29288372,33.3615873 8.75172093,32.8211429 8.75172093,32.1552381 C8.75172093,31.4887302 9.29288372,30.9488889 9.96102326,30.9488889 L16.0075349,30.9488889 C16.6750698,30.9488889 17.2168372,31.4887302 17.2168372,32.1552381 C17.2168372,32.8211429 16.6750698,33.3615873 16.0075349,33.3615873" fill="#BBE2EE" />
<path id="Fill-135" d="M16.0075349,33.3615873 L16.0075349,32.7584127 L9.96102326,32.7584127 C9.62725581,32.7578095 9.35697674,32.4881905 9.35637209,32.1552381 C9.35697674,31.8222857 9.62725581,31.5526667 9.96102326,31.5520635 L16.0075349,31.5520635 C16.3406977,31.5526667 16.6115814,31.8222857 16.612186,32.1552381 C16.6115814,32.4881905 16.3406977,32.7578095 16.0075349,32.7584127 L16.0075349,33.9647619 C17.0094419,33.9641587 17.8208837,33.1546984 17.8214884,32.1552381 C17.8208837,31.1557778 17.0094419,30.3463175 16.0075349,30.3457143 L9.96102326,30.3457143 C8.95851163,30.3463175 8.14706977,31.1557778 8.14706977,32.1552381 C8.14706977,33.1546984 8.95851163,33.9641587 9.96102326,33.9647619 L16.0075349,33.9647619 L16.0075349,33.3615873" fill="#5CBBDE" />
</g>
<g id="Group-4" transform="translate(195.373740, 35.038750) rotate(6.000000) translate(-195.373740, -35.038750) translate(174.873740, 16.038750)">
<path id="Fill-186" d="M38.673403,30.1261587 L31.5039104,20.0615873 C31.0455672,19.4186032 30.2977761,19.0355873 29.5004179,19.0355873 L25.1580896,19.0355873 L25.1580896,15.4165397 C25.1580896,14.7500317 24.609791,14.2101905 23.934209,14.2101905 C23.2580149,14.2101905 22.7103284,14.7500317 22.7103284,15.4165397 L22.7103284,19.0355873 L18.8098209,19.0355873 L18.8098209,15.4165397 C18.8098209,14.7500317 18.2615224,14.2101905 17.5859403,14.2101905 C16.9097463,14.2101905 16.3620597,14.7500317 16.3620597,15.4165397 L16.3620597,19.0355873 L12.0191194,19.0355873 C11.2217612,19.0355873 10.4745821,19.4186032 10.0162388,20.0615873 L2.84613433,30.1261587 C2.55668657,30.5326984 2.40186567,31.0164444 2.40186567,31.5128571 L2.40186567,34.718127 C2.40186567,36.0505397 3.49785075,37.1308254 4.84962687,37.1308254 L36.6705224,37.1308254 C38.0222985,37.1308254 39.1182836,36.0505397 39.1182836,34.718127 L39.1182836,31.5128571 C39.1182836,31.0164444 38.9628507,30.5326984 38.673403,30.1261587" fill="#BBE2EE" />
<path id="Fill-187" d="M38.673403,30.1261587 L39.1745821,29.7799365 L32.0044776,19.7147619 C31.4317015,18.9107302 30.4972687,18.4324127 29.5004179,18.4324127 L25.1580896,18.4324127 L25.1580896,19.0355873 L25.7700299,19.0355873 L25.7700299,15.4165397 C25.7694179,14.4164762 24.948194,13.6070159 23.934209,13.6070159 C22.9196119,13.6070159 22.0983881,14.4164762 22.0983881,15.4165397 L22.0983881,19.0355873 L22.7103284,19.0355873 L22.7103284,18.4324127 L18.8098209,18.4324127 L18.8098209,19.0355873 L19.4217612,19.0355873 L19.4217612,15.4165397 C19.4211493,14.4164762 18.5999254,13.6070159 17.5859403,13.6070159 C16.5713433,13.6070159 15.7501194,14.4164762 15.7501194,15.4165397 L15.7501194,19.0355873 L16.3620597,19.0355873 L16.3620597,18.4324127 L12.0191194,18.4324127 C11.0222687,18.4324127 10.0878358,18.9107302 9.5150597,19.7153651 L2.34556716,29.7799365 C1.98391045,30.2872063 1.78992537,30.8927937 1.78992537,31.5128571 L1.78992537,34.718127 C1.78992537,36.3840952 3.15944776,37.7333968 4.84962687,37.734 L36.6705224,37.734 C38.3607015,37.7333968 39.7296119,36.3840952 39.7302239,34.718127 L39.7302239,31.5128571 C39.7302239,30.8927937 39.5362388,30.2872063 39.1745821,29.7799365 L38.673403,30.1261587 L38.1728358,30.4729841 C38.3894627,30.7775873 38.5063433,31.1406984 38.5063433,31.5128571 L38.5063433,34.718127 C38.5045075,35.7169841 37.6838955,36.5258413 36.6705224,36.5276508 L4.84962687,36.5276508 C3.83564179,36.5258413 3.01564179,35.7169841 3.01380597,34.718127 L3.01380597,31.5128571 C3.01380597,31.1406984 3.13007463,30.7775873 3.34670149,30.4729841 L10.516806,20.4084127 C10.8607164,19.925873 11.4212537,19.6387619 12.0191194,19.6387619 L16.3620597,19.6387619 C16.523,19.6387619 16.6802687,19.5742222 16.7947015,19.4620317 C16.9085224,19.3498413 16.974,19.1942222 16.974,19.0355873 L16.974,15.4165397 C16.9746119,15.0835873 17.2481493,14.8139683 17.5859403,14.8133651 C17.9231194,14.8139683 18.1972687,15.0835873 18.1978806,15.4165397 L18.1978806,19.0355873 C18.1978806,19.1942222 18.2627463,19.3498413 18.3765672,19.4620317 C18.491,19.5742222 18.6482687,19.6387619 18.8098209,19.6387619 L22.7103284,19.6387619 C22.8712687,19.6387619 23.0291493,19.5742222 23.1429701,19.4620317 C23.256791,19.3498413 23.3222687,19.1942222 23.3222687,19.0355873 L23.3222687,15.4165397 C23.3228806,15.0835873 23.5964179,14.8139683 23.934209,14.8133651 C24.2713881,14.8139683 24.5455373,15.0835873 24.5461493,15.4165397 L24.5461493,19.0355873 C24.5461493,19.1942222 24.6110149,19.3498413 24.7248358,19.4620317 C24.8392687,19.5742222 24.9965373,19.6387619 25.1580896,19.6387619 L29.5004179,19.6387619 C30.0982836,19.6387619 30.6594328,19.925873 31.0027313,20.4084127 L38.1728358,30.4729841 L38.673403,30.1261587" fill="#5CBBDE" />
<path id="Fill-188" d="M40.3360448,10.8794603 C40.3360448,10.8794603 41.2612985,0.940349206 20.7600746,0.940349206 L20.7594627,0.970507937 L20.7594627,0.940349206 C0.314537313,0.940349206 1.18349254,10.8794603 1.18349254,10.8794603 L1.18349254,13.148 C1.18349254,13.7348889 1.78870149,14.2101905 2.53465672,14.2101905 L7.93808955,14.2101905 C8.68404478,14.2101905 9.28864179,13.7348889 9.28864179,13.148 L9.28864179,8.96980952 C9.28864179,8.96980952 13.919806,7.45765079 20.7594627,7.45765079 C27.5997313,7.45765079 32.2308955,8.96980952 32.2308955,8.96980952 L32.2308955,13.148 C32.2308955,13.7348889 32.8354925,14.2101905 33.5820597,14.2101905 L38.9848806,14.2101905 C39.7314478,14.2101905 40.3360448,13.7348889 40.3360448,13.148 L40.3360448,10.8794603" fill="#BBE2EE" />
<path id="Fill-189" d="M40.3360448,10.8794603 L40.9455373,10.9343492 C40.9467612,10.9174603 40.9541045,10.8312063 40.9541045,10.6864444 C40.9541045,10.2847302 40.8984179,9.4312381 40.5012687,8.37025397 C39.9089104,6.77847619 38.5247015,4.73190476 35.5108955,3.12866667 C32.4970896,1.52180952 27.8787761,0.337777778 20.7600746,0.337174603 C20.427791,0.337174603 20.1554776,0.60015873 20.1487463,0.92768254 L20.1481343,0.95784127 L20.7594627,0.970507937 L21.371403,0.963873016 L21.371403,0.933714286 C21.3677313,0.602571429 21.0954179,0.337174603 20.7594627,0.337174603 C16.0022388,0.337174603 12.3642537,0.869174603 9.57319403,1.7027619 C5.38752239,2.94952381 3.10008955,4.89657143 1.89456716,6.70247619 C0.687208955,8.50657143 0.566044776,10.1297143 0.566044776,10.705746 C0.566044776,10.8372381 0.572776119,10.9156508 0.574,10.9319365 C0.601537313,11.2437778 0.865895522,11.4826349 1.18349254,11.4826349 L1.18349254,10.8794603 L1.06355224,10.2883492 C0.777164179,10.3444444 0.571552239,10.591746 0.571552239,10.8794603 L0.571552239,13.148 C0.570940299,13.652254 0.836522388,14.0901587 1.19756716,14.3706349 C1.5610597,14.6553333 2.02980597,14.8127619 2.53465672,14.8133651 L7.93808955,14.8133651 C8.4429403,14.8127619 8.91107463,14.6553333 9.27456716,14.3706349 C9.63622388,14.0901587 9.90119403,13.652254 9.90058209,13.148 L9.90058209,8.96980952 L9.28864179,8.96980952 L9.48140299,9.5428254 L9.52668657,9.52834921 C10.0186866,9.37453968 14.4332239,8.06022222 20.7594627,8.0608254 C24.1337015,8.0608254 26.9645373,8.43419048 28.946,8.80634921 C29.9367313,8.99212698 30.7151194,9.17790476 31.2432239,9.31663492 C31.5069701,9.386 31.7082985,9.44330159 31.8429254,9.48250794 C31.9096269,9.5024127 31.959806,9.51809524 31.9928507,9.52834921 L32.0381343,9.5428254 L32.2308955,8.96980952 L31.6189552,8.96980952 L31.6189552,13.148 C31.6183433,13.652254 31.8839254,14.0901587 32.2449701,14.3706349 C32.6084627,14.6553333 33.076597,14.8127619 33.5820597,14.8133651 L38.9848806,14.8133651 C39.4903433,14.8127619 39.9584776,14.6553333 40.3219701,14.3706349 C40.6830149,14.0901587 40.948597,13.652254 40.9479851,13.148 L40.9479851,10.8794603 C40.9479851,10.591746 40.7423731,10.3444444 40.4559851,10.2883492 L40.3360448,10.8794603 L40.3360448,11.4826349 C40.651806,11.4826349 40.9167761,11.244381 40.9455373,10.9343492 L40.3360448,10.8794603 L40.3360448,10.2762857 C40.0221194,10.2762857 39.7577612,10.512127 39.7271642,10.8203492 C39.6959552,11.1279683 39.9082985,11.410254 40.2161045,11.4711746 L40.3360448,10.8794603 L39.7241045,10.8794603 L39.7241045,13.148 C39.7234925,13.2300317 39.6867761,13.3235238 39.5588806,13.4272698 C39.4334328,13.5273968 39.2259851,13.607619 38.9848806,13.6070159 L33.5820597,13.6070159 C33.3403433,13.607619 33.1335075,13.5273968 33.0080597,13.4272698 C32.8801642,13.3235238 32.8434478,13.2306349 32.8428358,13.148 L32.8428358,8.96980952 C32.8428358,8.7092381 32.6739403,8.47942857 32.4230448,8.39739683 C32.380209,8.38412698 27.6915224,6.85507937 20.7594627,6.85447619 C13.8280149,6.85507937 9.13932836,8.38412698 9.09649254,8.39739683 C8.84559701,8.47942857 8.67670149,8.7092381 8.67670149,8.96980952 L8.67670149,13.148 C8.67608955,13.2306349 8.63937313,13.3235238 8.51147761,13.4272698 C8.38602985,13.5273968 8.17919403,13.607619 7.93808955,13.6070159 L2.53465672,13.6070159 C2.29355224,13.607619 2.08610448,13.5273968 1.96065672,13.4272698 C1.83276119,13.3235238 1.79604478,13.2300317 1.79543284,13.148 L1.79543284,10.8794603 L1.18349254,10.8794603 L1.30343284,11.4711746 L1.30404478,11.4711746 C1.61185075,11.410254 1.82358209,11.1279683 1.79298507,10.8203492 C1.76177612,10.512127 1.49741791,10.2762857 1.18349254,10.2762857 L1.18349254,10.8794603 L1.79298507,10.8275873 L1.74770149,10.8312063 L1.79359701,10.8281905 L1.79298507,10.8275873 L1.74770149,10.8312063 L1.79359701,10.8281905 C1.79237313,10.8185397 1.78992537,10.7763175 1.78992537,10.705746 C1.78992537,10.4300952 1.82908955,9.7255873 2.16259701,8.82987302 C2.66683582,7.48660317 3.80932836,5.70301587 6.57591045,4.2107619 C9.34310448,2.72152381 13.7613134,1.54292063 20.7594627,1.54352381 L20.7594627,0.940349206 L20.1475224,0.946984127 L20.1475224,0.977142857 C20.151194,1.30647619 20.4222836,1.57187302 20.756403,1.57368254 C21.0911343,1.57488889 21.3646716,1.31190476 21.371403,0.982571429 L21.3720149,0.953015873 L20.7600746,0.940349206 L20.7600746,1.54352381 C25.4151045,1.54292063 28.9313134,2.06165079 31.5767313,2.84577778 C35.5463881,4.02619048 37.5492687,5.7832381 38.5950746,7.32796825 C39.6384328,8.87390476 39.7302239,10.2479365 39.7302239,10.6864444 L39.7277761,10.8010476 L39.7265522,10.8245714 L39.7338955,10.8251746 L39.7265522,10.8245714 L39.7338955,10.8251746 L39.7265522,10.8245714 L40.3360448,10.8794603 L40.3360448,10.2762857 L40.3360448,10.8794603" fill="#5CBBDE" />
<path id="Fill-190" d="M26.8794776,25.6705079 C26.8794776,27.335873 24.1398209,28.686381 20.7600746,28.686381 C17.3803284,28.686381 14.6406716,27.335873 14.6406716,25.6705079 C14.6406716,24.0051429 17.3803284,22.6546349 20.7600746,22.6546349 C24.1398209,22.6546349 26.8794776,24.0051429 26.8794776,25.6705079" fill="#FFFFFE" />
<path id="Fill-191" d="M26.8794776,25.6705079 L26.2675373,25.6705079 C26.2663134,25.919619 26.1702388,26.1729524 25.9309701,26.4546349 C25.5760448,26.8744444 24.8906716,27.3008889 23.9856119,27.5994603 C23.0811642,27.9010476 21.9649851,28.0832063 20.7600746,28.0832063 C19.1531194,28.0844127 17.7028209,27.7568889 16.7065821,27.2634921 C16.2078507,27.0186032 15.826,26.7333016 15.5885672,26.4546349 C15.3492985,26.1729524 15.2532239,25.919619 15.2526119,25.6705079 C15.2532239,25.4213968 15.3492985,25.1680635 15.5885672,24.886381 C15.9434925,24.4665714 16.6288657,24.040127 17.5345373,23.7409524 C18.4383731,23.4399683 19.5545522,23.2578095 20.7600746,23.2578095 C22.3664179,23.256 23.8167164,23.584127 24.8135672,24.0775238 C25.3116866,24.3224127 25.6935373,24.6077143 25.9309701,24.886381 C26.1702388,25.1680635 26.2663134,25.4213968 26.2675373,25.6705079 L27.4914179,25.6705079 C27.4920299,25.0866349 27.2454179,24.5467937 26.865403,24.1070794 C26.2920149,23.4441905 25.4255075,22.9501905 24.3772537,22.5985397 C23.3277761,22.2493016 22.089209,22.0520635 20.7600746,22.0514603 C18.9866716,22.0532698 17.3772687,22.4000952 16.1588955,22.9984444 C15.5506269,23.2994286 15.0359851,23.663746 14.6541343,24.1070794 C14.2741194,24.5467937 14.0275075,25.0866349 14.0287313,25.6705079 C14.0275075,26.254381 14.2741194,26.7942222 14.6541343,27.2339365 C15.2281343,27.8968254 16.0946418,28.3908254 17.1428955,28.7424762 C18.1917612,29.0917143 19.4303284,29.2889524 20.7600746,29.2895556 C22.5328657,29.287746 24.1422687,28.9403175 25.3606418,28.3425714 C25.9695224,28.0415873 26.4841642,27.6772698 26.865403,27.2339365 C27.2454179,26.7942222 27.4920299,26.254381 27.4914179,25.6705079 L26.8794776,25.6705079" fill="#CCD4D3" />
</g>
<g id="Group-5" transform="translate(67.000000, 43.000000)" stroke="#F5BB27" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(42.000000, 2.000000)" stroke="#A2B88C" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(22.000000, 51.000000)" stroke="#E5742B" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(0.000000, 17.000000)" stroke="#5CBBDE" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(414.000000, 37.500000) scale(-1, -1) translate(-414.000000, -37.500000) translate(409.000000, 32.000000)" stroke="#F5BB27" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(386.000000, 5.500000) scale(-1, -1) translate(-386.000000, -5.500000) translate(381.000000, 0.000000)" stroke="#A2B88C" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
<g id="Group-5" transform="translate(386.000000, 64.500000) scale(-1, -1) translate(-386.000000, -64.500000) translate(381.000000, 59.000000)" stroke="#E5742B" stroke-linecap="square" stroke-width="3">
<path id="Line" d="M0.277777778,5.5 L9.73854798,5.5" />
<path id="Line" d="M5.5,10.6764706 L5.5,0.323529412" />
</g>
</g>
</g>
</svg>
</div>
<form @submit.prevent="update()">
<div class="mb4">
<div v-if="limited" class="mt3 mb3 form-information-message br2">
<div class="pa3 flex">
<div class="mr3">
<svg viewBox="0 0 20 20">
<g fill-rule="evenodd">
<circle cx="10" cy="10" r="9" fill="currentColor" /><path d="M10 0C4.486 0 0 4.486 0 10s4.486 10 10 10 10-4.486 10-10S15.514 0 10 0m0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8m1-5v-3a1 1 0 0 0-1-1H9a1 1 0 1 0 0 2v3a1 1 0 0 0 1 1h1a1 1 0 1 0 0-2m-1-5.9a1.1 1.1 0 1 0 0-2.2 1.1 1.1 0 0 0 0 2.2" />
</g>
</svg>
</div>
<div v-html="$t('settings.personalisation_paid_upgrade_vue', {url: 'settings/subscriptions' })"></div>
</div>
</div>
<p class="mt3 b mb3" :class="[ dirltr ? 'tl' : 'tr' ]">
{{ $t('people.stay_in_touch_modal_desc', { firstname: firstName }) }}
</p>
<div class="mb2">
<toggle-button class="mr2" :sync="true" :labels="true" :value="stateInput" @change="stateInput = !stateInput" />
<div class="dib relative" style="top: -2px;">
<stay-in-touch-label
v-model="frequencyInput"
:class="{ 'form-group-error': $v.frequencyInput.$error }"
>
<div class="dib">
<form-input
:id="'frequency'"
v-model.number="frequencyInput"
:input-type="'number'"
:width="60"
:required="true"
:validator="$v.frequencyInput"
@input="onInput($event)"
/>
</div>
</stay-in-touch-label>
</div>
</div>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
</div>
</div>
</div>
</form>
<div slot="button" class="tc">
<a class="btn" href="" @click.prevent="closeModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="update()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
import { ToggleButton } from 'vue-js-toggle-button';
import { validationMixin } from 'vuelidate';
import { required, numeric } from 'vuelidate/lib/validators';
import StayInTouchLabel from './StayInTouchLabel';
export default {
components: {
SweetModal,
ToggleButton,
StayInTouchLabel,
},
mixins: [validationMixin],
props: {
hash: {
type: String,
default: '',
},
firstName: {
type: String,
default: '',
},
frequency: {
type: Number,
default: 0,
},
triggerDate: {
type: String,
default: null,
},
limited: {
type: Boolean,
default: false,
},
},
validations: {
frequencyInput: {
required,
numeric,
},
},
data() {
return {
isActive: false,
errorMessage: '',
frequencyInput: 0,
nextTriggerDate: null,
stateInput: false,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.isActive = (this.frequency > 0);
// record initial values when the component loads so we can
// put those values back if user puts wrong values when updating
// the counter
this.stateInput = this.isActive;
this.frequencyInput = this.frequency ?? 0;
this.nextTriggerDate = this.triggerDate;
},
formatDate(dateAsString) {
const moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var date = moment.tz(moment(dateAsString), this.$root.timezone);
return date.format('LL');
},
showUpdate() {
this.errorMessage = '';
this.$refs.updateModal.open();
},
closeModal() {
this.$refs.updateModal.close();
},
update() {
this.errorMessage = '';
// check if you need a subscription to access this feature
if (this.limited) {
this.errorMessage = this.$t('people.stay_in_touch_premium');
return;
}
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
axios.post('people/' + this.hash + '/stayintouch', {frequency: this.frequencyInput, state: this.stateInput})
.then(response => {
this.$refs.updateModal.close();
this.isActive = this.stateInput;
this.nextTriggerDate = response.data.trigger_date;
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
width: '500px',
type: 'success'
});
})
.catch(error => {
this.errorMessage = this.$t('app.error_save');
});
},
onInput(value) {
this.stateInput = value > 0;
}
}
};
</script>

View File

@@ -0,0 +1,20 @@
<script>
export default {
props: {
value: {
type: Number,
default: 0,
},
},
render(createElement) {
const text = this.$tc('people.stay_in_touch_modal_label', this.value, {count: '[slot]'});
const texts = _.split(text, '[slot]');
return createElement('div', [
createElement('span', texts[0]),
this.$slots.default,
createElement('span', texts[1]),
]);
},
};
</script>

View File

@@ -0,0 +1,231 @@
<style scoped>
.autocomplete-results {
width: 150px;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4AAE9B;
color: white;
}
.tag-link,
.tag-link:hover{
text-decoration: none;
}
</style>
<template>
<div class="tc">
<!-- list of existing tags -->
<ul>
<li v-for="tag in contactTags" :key="tag.id" class="di mr2">
<a v-if="!editMode" :href="`people?tags[]=${encodeURIComponent(tag.name)}`" class="tag-link bg-white ph2 pb1 pt0 dib br3 b--light-gray ba mb2">
{{ tag.name }}
</a>
<span v-else class="bg-white ph2 pb1 pt0 dib br3 b--light-gray ba mb2">
<span>
{{ tag.name }}
</span>
<span class="pointer" @click="removeTag(tag)">
×
</span>
</span>
</li>
<!-- edit button -->
<li v-show="contactTags.length > 0" class="di">
<a v-show="!editMode" class="pointer" href="" @click.prevent="enterEditMode">
{{ $t('app.edit') }}
</a>
</li>
<!-- add a new tag -->
<li v-show="editMode" class="di mb3">
<div class="relative di mr2">
<input ref="tags"
v-model="search"
type="text"
class="di br2 f5 ba b--black-40 pa2 outline-0"
:placeholder="$t('people.tag_add_search')"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter"
@keydown.esc="onEscape"
@input="onChange"
/>
<ul v-show="isOpen" v-if="results.length > 0" class="autocomplete-results ba b--gray-monica absolute bg-white left-0 z-9999">
<li v-for="(result, i) in results"
:key="i"
class="autocomplete-result"
:class="{ 'is-active': i === arrowCounter }"
@click="setResult(result)"
>
{{ result.name }}
</li>
</ul>
</div>
<a class="pointer" href="" @click.prevent="search = ''; editMode = false; isOpen = false;">
{{ $t('app.close') }}
</a>
</li>
<!-- case of no tags -->
<li v-show="contactTags.length === 0 && !editMode" class="di">
<span class="i mr2">
{{ $t('people.tag_no_tags') }}
</span>
<a v-show="!editMode" class="pointer" href="" @click.prevent="enterEditMode">
{{ $t('people.tag_add') }}
</a>
</li>
</ul>
</div>
</template>
<script>
import moment from 'moment';
export default {
props: {
hash: {
type: String,
default: '',
},
},
data() {
return {
allTags: [],
availableTags: [],
contactTags: [],
editMode: false,
search: '',
results: [],
isOpen: false,
arrowCounter: 0,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
document.addEventListener('click', this.handleClickOutside);
},
destroyed() {
document.removeEventListener('click', this.handleClickOutside);
},
methods: {
prepareComponent() {
this.getExistingTags();
this.getContactTags();
},
getExistingTags() {
axios.get('tags')
.then(response => {
this.allTags = response.data.data;
});
},
getContactTags() {
axios.get('people/' + this.hash + '/tags')
.then(response => {
this.contactTags = response.data.data;
});
},
enterEditMode() {
this.editMode = true;
this.$nextTick(() => this.$refs.tags.focus());
},
removeTag(tag) {
this.contactTags.splice(this.contactTags.indexOf(tag), 1);
this.store();
},
onChange() {
this.isOpen = true;
this.filterResults();
},
onEnter() {
if (this.search !== '') {
this.contactTags.push({
id: moment().format(), // we just need a random ID here
name: this.search
});
this.arrowCounter = -1;
this.isOpen = false;
this.search = '';
this.store();
}
},
onArrowDown() {
if (this.arrowCounter < this.results.length) {
this.arrowCounter = this.arrowCounter + 1;
this.search = this.results[this.arrowCounter].name;
}
},
onArrowUp() {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter - 1;
this.search = this.results[this.arrowCounter].name;
}
},
onEscape() {
this.arrowCounter = -1;
this.isOpen = false;
this.search = '';
},
setResult(result) {
this.search = '';
this.isOpen = false;
this.contactTags.push(result);
this.store();
},
filterResults() {
var me = this.contactTags;
var search = _.toLower(this.search);
this.results = this.allTags.filter(item => _.toLower(item.name).indexOf(search) > -1
&& _.findIndex(me, t => t.name === item.name) < 0);
},
filterAllTags() {
var me = this.contactTags;
this.availableTags = this.allTags.filter((item) => {
return !me.includes(item);
});
},
store() {
axios.post('people/' + this.hash + '/tags/update', this.contactTags)
.then(response => {
this.getExistingTags();
});
},
handleClickOutside(evt) {
if (!this.$el.contains(evt.target)) {
this.isOpen = false;
this.arrowCounter = -1;
}
}
}
};
</script>

View File

@@ -0,0 +1,286 @@
<template>
<div>
<div>
<img src="img/people/tasks.svg" :alt="$t('people.tasks_title')" class="icon-section icon-tasks" />
<h3>
{{ $t('people.section_personal_tasks') }}
<span v-if="tasks.length !== 0" class="f6 pt2" :class="[ dirltr ? 'fr' : 'fl' ]">
<a v-if="!editMode" v-cy-name="'task-toggle-edit-mode'" class="pointer" href="" @click.prevent="editMode = true">
{{ $t('app.edit') }}
</a>
<a v-else v-cy-name="'task-toggle-edit-mode'" class="pointer" href="" @click.prevent="editMode = false">
{{ $t('app.done') }}
</a>
</span>
</h3>
</div>
<div :class="[editMode ? 'bg-washed-yellow b--yellow ba pa2' : '']">
<!-- EMPTY STATE -->
<div v-if="tasks.length === 0 && !addMode" v-cy-name="'task-blank-state'" class="tc bg-near-white b--moon-gray pa3">
<p>{{ $t('people.tasks_blank_title') }}</p>
<p>
<a v-cy-name="'add-task-button'" class="pointer" href="" @click.prevent="toggleAddMode">
{{ $t('people.tasks_add_task') }}
</a>
</p>
</div>
<!-- LIST OF IN PROGRESS TASKS -->
<ul v-cy-name="'tasks-body'" v-cy-items="inProgress(tasks).map(t => t.id)">
<li v-for="(task, i) in inProgress(tasks)" :key="task.id" v-cy-name="'task-item-' + task.id">
<form-checkbox
v-model.lazy="task.completed"
:disabled="task.disabled"
:name="'task'"
:dclass="[ dirltr ? 'mr1' : 'ml1' ]"
@change="toggleComplete(task)"
>
{{ task.title }}
<span v-if="task.description" class="silver ml3" dir="auto">
{{ task.description }}
</span>
</form-checkbox>
<div v-if="editMode" class="di">
<em class="fa fa-pencil-square-o pointer pr2 ml3 dark-blue" @click="toggleEditMode(task)"></em>
<em v-cy-name="'task-delete-button-' + task.id" class="fa fa-trash-o pointer pr2 dark-blue" @click="trash(task)"></em>
</div>
<!-- EDIT BOX -->
<form v-show="task.edit" class="bg-near-white pa2 br2 mt3 mb3">
<div>
<label :for="'edit-title' + i" class="db fw6 lh-copy f6">
{{ $t('people.tasks_form_title') }}
</label>
<input :id="'edit-title' + i" v-model="task.title" class="pa2 db w-100" type="text" @keyup.esc="editMode = false" />
</div>
<div class="mt3">
<label :for="'edit-description' + i" class="db fw6 lh-copy f6">
{{ $t('people.tasks_form_description') }}
</label>
<textarea :id="'edit-description' + i"
v-model="task.description"
class="pa2 db w-100"
type="text"
@keyup.esc="editMode = false"
>
</textarea>
</div>
<div class="lh-copy mt3">
<a class="btn btn-primary" href="" @click.prevent="update(task, true)">
{{ $t('app.update') }}
</a>
<a class="btn" href="" @click.prevent="toggleEditMode(task)">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</li>
</ul>
<!-- ADD TASK TO ENTER ADD MODE -->
<div v-if="!updateMode && !addMode && tasks.length !== 0" class="bg-near-white pa2 br2 mt3 mb3">
<a class="pointer" href="" @click.prevent="toggleAddMode">
{{ $t('people.tasks_add_task') }}
</a>
</div>
<!-- ADD A TASK VIEW -->
<div v-if="addMode" v-cy-name="'task-add-view'">
<form class="bg-near-white pa2 br2 mt3 mb3">
<div>
<label for="add-title" class="db fw6 lh-copy f6">
{{ $t('people.tasks_form_title') }}
</label>
<input id="add-title" v-model="newTask.title" v-cy-name="'task-add-title'" class="pa2 db w-100" type="text"
@keyup.esc="addMode = false"
/>
</div>
<div class="mt3">
<label for="add-description" class="db fw6 lh-copy f6">
{{ $t('people.tasks_form_description') }}
</label>
<textarea id="add-description" v-model="newTask.description" class="pa2 db w-100" type="text" @keyup.esc="addMode = false"></textarea>
</div>
<div class="lh-copy mt3">
<a v-cy-name="'save-task-button'" class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.add') }}
</a>
<a class="btn" href="" @click.prevent="addMode = false">
{{ $t('app.cancel') }}
</a>
</div>
</form>
</div>
<!-- LIST OF COMPLETED TASKS -->
<ul>
<li v-for="task in completed(tasks)" :key="task.id" v-cy-name="'task-item-completed-' + task.id" class="f6">
<form-checkbox
v-model.lazy="task.completed"
:disabled="task.disabled"
:name="'checkbox'"
:dclass="[ dirltr ? 'mr1' : 'ml1' ]"
@change="toggleComplete(task)"
>
<span class="light-silver mr1">
{{ task.completed_at }}
</span>
<span class="moon-gray" dir="auto">
{{ task.title }}
</span>
<span v-if="task.description" class="silver ml3" dir="auto">
{{ task.description }}
</span>
</form-checkbox>
<div v-if="editMode" class="di">
<em class="fa fa-trash-o pointer pr2 ml3 dark-blue" @click="trash(task)"></em>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: -1,
},
},
data() {
return {
tasks: [],
updateMode: false,
addMode: false,
editMode: false,
newTask: {
contact_id: 0,
title: '',
description: '',
completed: 0
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.newTask.contact_id = this.contactId;
this.index();
},
methods: {
reinitialize() {
this.newTask.title = '';
this.newTask.description = '';
},
completed: function (tasks) {
return tasks.filter(function (task) {
return task.completed === true;
});
},
inProgress: function (tasks) {
return tasks.filter(function (task) {
return task.completed === false;
});
},
toggleAddMode() {
this.addMode = true;
this.reinitialize();
},
toggleEditMode(task) {
Vue.set(task, 'edit', !task.edit);
},
index() {
axios.get('people/' + this.hash + '/tasks')
.then(response => {
this.tasks = _.map(response.data, function (task) {
return _.assign({}, task, {disabled: false});
});
});
},
store() {
axios.post('tasks', this.newTask)
.then(response => {
this.addMode = false;
this.reinitialize();
this.tasks.push(response.data);
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
toggleComplete(task) {
this.updateMode = true;
Vue.set(task, 'disabled', true);
this.update(task, false);
},
update(task, toggleEdit) {
axios.put('tasks/' + task.id, task)
.then(response => {
this.updateMode = false;
Vue.set(task, 'disabled', false);
Vue.set(task, 'completed_at', response.data.completed_at ? this.formatDate(response.data.completed_at): null);
if (toggleEdit) {
this.toggleEditMode(task);
}
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
formatDate(dateAsString) {
const moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var date = moment.tz(moment(dateAsString), this.$root.timezone);
return date.format('ll');
},
trash(task) {
axios.delete('tasks/' + task.id)
.then(response => {
this.tasks.splice(this.tasks.indexOf(task), 1);
});
if (this.tasks.length <= 1) {
this.editMode = false;
}
},
}
};
</script>

View File

@@ -0,0 +1,237 @@
<style scoped>
.btn-title {
top: -7px;
}
</style>
<template>
<div>
<div class="">
<h3 class="mb2">
🍿&#8199;{{ $t('people.activity_title') }}
<span class="fr relative btn-title">
<a v-if="displayLogActivity === false" v-cy-name="'add-activity-button'" class="btn edit-information" @click="displayLogActivity = true">
{{ $t('people.activities_add_activity') }}
</a>
<a v-else class="btn edit-information" @click="displayLogActivity = false">
{{ $t('app.cancel') }}
</a>
</span>
</h3>
</div>
<!-- BLANK STATE -->
<div v-if="!displayLogActivity && activities.length === 0" class="w-100">
<div v-cy-name="'activities-blank-state'" class="bg-near-white tc pa3 br2 ba b--light-gray">
<p>{{ $t('people.activities_blank_title', { name: name }) }}</p>
<a class="pointer" href="" @click.prevent="displayLogActivity = true">
{{ $t('people.activities_blank_add_activity') }}
</a>
</div>
</div>
<!-- LOG AN ACTIVITY -->
<template v-if="displayLogActivity">
<create-activity
:hash="hash"
:contact-id="contactId"
:name="name"
@update="updateList($event)"
@cancel="displayLogActivity = false"
/>
</template>
<!-- LIST OF ACTIVITIES -->
<div v-cy-name="'activities-body'" v-cy-items="activities.map(c => c.id)">
<div v-for="activity in activities" :key="activity.id" v-cy-name="'activity-body-'+activity.id" class="ba br2 b--black-10 br--top w-100 mb2">
<template v-if="!activity.edit">
<h2 class="pl2 pr2 pt3 f5">
{{ activity.summary }}
</h2>
<div v-if="activity.description" dir="auto" class="markdown pl2 pr2 pb3" v-html="compiledMarkdown(activity.description)">
</div>
<!-- DETAILS -->
<div class="pa2 cf bt b--black-10 br--bottom f7">
<div class="w-70" :class="[ dirltr ? 'fl' : 'fr' ]">
<ul class="list">
<!-- HAPPENED AT -->
<li class="di" :class="[ dirltr ? 'mr3' : 'ml3' ]">
{{ activity.happened_at | moment }}
</li>
<!-- PARTICIPANT LIST -->
<li v-if="activity.attendees.total > 1" class="di">
<ul class="di list" :class="[ dirltr ? 'mr3' : 'ml3' ]">
<li class="di">
{{ $t('people.activities_list_participants', { total: activity.attendees.total - 1}) }}
</li>
<li v-for="attendee in activity.attendees.contacts.filter(c => c.id !== contactId)" :key="attendee.id" class="di mr2">
<a :href="'people/' + attendee.hash_id">{{ attendee.complete_name }}</a>
</li>
</ul>
</li>
<!-- EMOTIONS LIST -->
<li v-if="activity.emotions.length !== 0" class="di">
<ul class="di list" :class="[ dirltr ? 'mr3' : 'ml3' ]">
<li class="di">
{{ $t('people.activities_list_emotions') }}
</li>
<li v-for="emotion in activity.emotions" :key="emotion.id" class="di">
{{ $t('app.emotion_' + emotion.name) }}
</li>
</ul>
</li>
<!-- ACTIVITY TYPE -->
<li v-if="activity.activity_type" class="di" :class="[ dirltr ? 'mr3' : 'ml3' ]">
{{ activity.activity_type.name }}
</li>
</ul>
</div>
<div class="w-30" :class="[ dirltr ? 'fl tr' : 'fr tl' ]">
<!-- ACTIONS -->
<ul class="list">
<li class="di">
<a v-cy-name="'edit-activity-button-'+activity.id" href="" class="pointer" @click.prevent="$set(activity, 'edit', true)">{{ $t('app.edit') }}</a>
<a v-show="destroyActivityId !== activity.id" v-cy-name="'delete-activity-button-'+activity.id" href="" class="pointer" @click.prevent="showDestroyActivity(activity)">{{ $t('app.delete') }}</a>
<ul v-show="destroyActivityId === activity.id" class="di">
<li class="di">
<a v-cy-name="'confirm-delete-activity'" class="pointer red" @click.prevent="destroyActivity(activity)">
{{ $t('app.delete_confirm') }}
</a>
</li>
<li class="di">
<a class="pointer mr1" @click.prevent="destroyActivityId = 0">
{{ $t('app.cancel') }}
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<!-- EDIT THE CURRENT ACTIVITY -->
<create-activity v-else
:hash="hash"
:name="name"
:activity="activity"
:contact-id="contactId"
@update="$set(activity, 'edit', false); updateList($event)"
@cancel="$set(activity, 'edit', false); displayLogActivity = false"
/>
</div>
<a v-if="!isLastPage" class="pointer mr1" style="float: right" @click.prevent="getActivities">
{{ $t('app.load_more') }}
</a>
</div>
<p v-if="activities.length > 0" class="tc">
📗 <a :href="'people/' + hash + '/activities/summary'">{{ $t('people.activities_view_activities_report') }}</a>
</p>
</div>
</template>
<script>
import moment from 'moment';
import CreateActivity from './CreateActivity.vue';
export default {
components: {
CreateActivity
},
filters: {
moment: function (date) {
return moment.utc(date).format('LL');
}
},
props: {
hash: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
}
},
data() {
return {
displayLogActivity: false,
activities: [],
emotions: [],
displayDescription: false,
displayEmotions: false,
displayCategory: false,
displayParticipants: false,
destroyActivityId: 0,
errors: [],
currentPage: 0,
lastPage: null
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
isLastPage () {
return this.lastPage === this.currentPage;
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getActivities();
this.todayDate = moment().format('YYYY-MM-DD');
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
},
getActivities() {
this.currentPage++;
axios.get('api/contacts/' + this.contactId + '/activities?page=' + this.currentPage)
.then(response => {
this.activities.push(...response.data.data);
this.currentPage = response.data.meta.current_page;
this.lastPage = response.data.meta.last_page;
});
},
updateList(activity) {
this.displayLogActivity = false;
const index = this.activities.indexOf(this.activities.find(item => item.id === activity.id));
Vue.set(this.activities, index >= 0 ? index : this.activities.length, activity);
},
showDestroyActivity(activity) {
this.destroyActivityId = activity.id;
},
destroyActivity(activity) {
axios.delete(`activities/${activity.id}`)
.then(response => {
this.activities.splice(this.activities.indexOf(activity), 1);
});
},
}
};
</script>

View File

@@ -0,0 +1,58 @@
<template>
<form-select
:id="'activity-type-list'"
v-model="choosenCategory"
:title="title"
:options="activityCategories"
:iclass="'br2 f5 w-100 ba b--black-40 pa2 outline-0'"
@input="$emit('input', $event)"
/>
</template>
<script>
export default {
props: {
value: {
type: [String, Number],
default: '',
},
title: {
type: String,
default: '',
},
},
data() {
return {
choosenCategory: '',
activityCategories: null,
};
},
watch: {
value(val) {
this.choosenCategory = val;
},
},
mounted() {
this.getActivities().then(() => {
this.choosenCategory = this.value;
});
},
methods: {
getActivities() {
return axios.get('activityCategories')
.then(response => {
this.activityCategories = Object.assign({}, _.map(response.data, a => {
return {
name: a.name,
options: a.types,
};
}));
});
},
}
};
</script>

View File

@@ -0,0 +1,297 @@
<template>
<div>
<!-- LOG AN ACTIVITY -->
<transition name="fade">
<div class="ba br3 mb3 pa3 b--black-40">
<div class="dt dt--fixed pb3 mb3 mb0-ns bb b--gray-monica">
<!-- SUMMARY -->
<div class="dtc pr2">
<form-input
:id="'summary'"
v-model="newActivity.summary"
:input-type="'text'"
:title="$t('people.activities_add_title', { name: name })"
:required="true"
/>
</div>
<!-- WHEN -->
<div class="dtc">
<p class="mb2 b">
{{ $t('people.activities_add_date_occured') }}
</p>
<div class="di">
<div class="dib">
<form-date
ref="date"
v-model="newActivity.happened_at"
:show-calendar-on-focus="true"
:default-date="todayDate"
:locale="locale"
/>
</div>
</div>
</div>
</div>
<!-- ADDITIONAL FIELDS -->
<div v-show="!displayDescription || !displayEmotions || !displayCategory || !displayParticipants" class="bb b--gray-monica pv3 mb3">
<ul class="list">
<li v-show="!displayDescription" class="di pointer mr3 nowrap-link">
<a href="" @click.prevent="displayDescription = true">{{ $t('people.activities_add_more_details') }}</a>
</li>
<li v-show="!displayEmotions" class="di pointer mr3 nowrap-link">
<a href="" @click.prevent="displayEmotions = true">{{ $t('people.activities_add_emotions') }}</a>
</li>
<li v-show="!displayCategory" class="di pointer mr3 nowrap-link">
<a v-cy-name="'activities_add_category'" href="" @click.prevent="displayCategory = true">{{ $t('people.activities_add_category') }}</a>
</li>
<li v-show="!displayParticipants" class="di pointer nowrap-link">
<a href="" @click.prevent="displayParticipants = true">{{ $t('people.activities_add_participants_cta') }}</a>
</li>
</ul>
</div>
<!-- DESCRIPTION -->
<div v-if="displayDescription" class="bb b--gray-monica pv3 mb3">
<form-textarea
v-model="newActivity.description"
:required="true"
:no-label="true"
:rows="4"
:title="$t('people.activities_summary')"
:placeholder="$t('people.conversation_add_content')"
@contentChange="updateDescription($event)"
/>
<p class="f6">
{{ $t('app.markdown_description') }} <a href="https://guides.github.com/features/mastering-markdown/" rel="noopener noreferrer" target="_blank">
{{ $t('app.markdown_link') }}
</a>
</p>
</div>
<!-- EMOTIONS -->
<div v-if="displayEmotions" class="bb b--gray-monica pb3 mb3">
<label>
{{ $t('people.activities_add_emotions_title') }}
</label>
<emotion
class="pv2"
:initial-emotions="initialEmotions"
@update="updateEmotionsList"
/>
</div>
<!-- ACTIVITY CATEGORIES -->
<div v-if="displayCategory" class="bb b--gray-monica pb3 mb3">
<activity-type-list
v-model="newActivity.activity_type_id"
:title="$t('people.activities_add_pick_activity')"
/>
</div>
<!-- PARTICPANTS -->
<div v-if="displayParticipants" class="bb b--gray-monica pb3 mb3">
<label>
{{ $t('people.activities_add_participants', {name: name}) }}
</label>
<participant
:hash="hash"
:initial-participants="participants"
@update="updateParticipant($event)"
/>
</div>
<error :errors="errors" />
<!-- ACTIONS -->
<div class="pt3">
<div class="flex-ns justify-between">
<div class="">
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" @click.prevent="close()">
{{ $t('app.cancel') }}
</a>
</div>
<div class="">
<button v-cy-name="'save-activity-button'" class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click.prevent="store()">
{{ activity ? $t('app.save') : $t('app.add') }}
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import moment from 'moment';
import ActivityTypeList from './ActivityTypeList.vue';
import Emotion from '../Emotion.vue';
import Error from '../../partials/Error.vue';
import Participant from '../Participant.vue';
export default {
components: {
ActivityTypeList,
Emotion,
Error,
Participant,
},
filters: {
moment: function (date) {
return moment.utc(date).format('LL');
}
},
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
},
name: {
type: String,
default: '',
},
activity: {
type: Object,
default: null,
},
},
data() {
return {
displayDescription: false,
displayEmotions: false,
displayCategory: false,
displayParticipants: false,
newActivity: {
summary: '',
description: '',
happened_at: '',
emotions: [],
activity_type_id: null,
contacts: [],
},
todayDate: '',
initialEmotions: [],
participants: [],
errors: [],
};
},
computed: {
locale() {
return this.$root.locale;
},
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
watch: {
participants(value) {
this.newActivity.contacts = _.map(value, p => p.id);
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.todayDate = moment().format('YYYY-MM-DD');
this.resetFields();
},
updateDescription(updatedDescription) {
this.newActivity.description = updatedDescription;
},
resetFields() {
if (this.activity) {
this.initialEmotions = JSON.parse(JSON.stringify(this.activity.emotions));
this.newActivity.summary = this.activity.summary;
this.newActivity.description = this.activity.description;
this.newActivity.happened_at = this.activity.happened_at;
this.updateEmotionsList(this.activity.emotions);
this.newActivity.activity_type_id = this.activity.activity_type ? this.activity.activity_type.id : null;
this.participants = this.activity.attendees.contacts.map(attendee => {
return {
id: attendee.id,
name: attendee.complete_name,
};
});
} else {
this.initialEmotions = [];
this.newActivity.summary = '';
this.newActivity.description = '';
this.newActivity.happened_at = this.todayDate;
this.newActivity.emotions = [];
this.newActivity.activity_type_id = null;
this.participants = [];
}
this.displayDescription = this.newActivity.description ? this.newActivity.description !== '' : false;
this.displayEmotions = this.newActivity.emotions && this.newActivity.emotions.length > 0;
this.displayCategory = this.newActivity.activity_type_id !== null;
this.displayParticipants = this.participants.length > 0;
this.errors = [];
},
close() {
this.resetFields();
this.$emit('cancel');
},
store() {
const method = this.activity ? 'put' : 'post';
const url = this.activity ? 'activities/'+this.activity.id : 'activities';
if (! this.newActivity.contacts.includes(this.contactId)) {
this.newActivity.contacts.push(this.contactId);
}
axios[method](url, this.newActivity)
.then(response => {
this.resetFields();
this.$emit('update', response.data.data);
this.$notify({
group: 'main',
title: this.$t('people.activities_add_success'),
text: '',
type: 'success'
});
})
.catch(error => {
this._errorHandle(error);
});
},
updateParticipant: function (participants) {
this.participants = participants;
},
updateEmotionsList: function(emotions) {
// filter the list of emotions to populate a new array
// containing only the emotion ids and not the entire objetcs
this.newActivity.emotions = _.map(emotions, emotion => emotion.id);
},
_errorHandle(error) {
if (error.response && typeof error.response.data === 'object') {
this.errors = _.flatten(_.toArray(error.response.data));
} else {
this.errors = [this.$t('app.error_try_again'), error.message];
}
},
}
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<span v-cy-name="'last-talked-to'">{{ lastCalledMessage }}</span>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
initialValue: {
type: String,
default: '',
}
},
data() {
return {
lastCalled: '',
};
},
computed: {
lastCalledMessage() {
if (!this.initialValue && !this.lastCalled) {
return this.$t('people.last_called_empty');
}
if (!this.lastCalled) {
return this.$t('people.last_talked_to', {date: this.initialValue});
}
return this.$t('people.last_talked_to', {date: this.lastCalled});
}
},
methods: {
getLastCalled() {
axios.get('people/' + this.hash + '/calls/last')
.then(response => {
this.lastCalled = response.data.last_talked_to;
});
}
}
};
</script>

View File

@@ -0,0 +1,417 @@
<template>
<div>
<div class="">
<h3 class="mb2">
&#8199;{{ $t('people.call_title') }}
<span class="fr relative" style="top: -7px;">
<a v-if="displayLogCall === false" v-cy-name="'add-call-button'" class="btn edit-information" href="" @click.prevent="displayLogCall = true">
{{ $t('people.call_button') }}
</a>
<a v-if="displayLogCall" class="btn edit-information" href="" @click.prevent="displayLogCall = false">
{{ $t('app.cancel') }}
</a>
</span>
</h3>
</div>
<!-- BLANK STATE -->
<div v-if="!displayLogCall && calls.length === 0" v-cy-name="'calls-blank-state'" class="w-100">
<div class="bg-near-white tc pa3 br2 ba b--light-gray">
<p>{{ $t('people.call_blank_title', { name: name }) }}</p>
<a class="pointer" href="" @click.prevent="displayLogCall = true">
{{ $t('people.call_button') }}
</a>
</div>
</div>
<!-- LOG A CALL -->
<transition name="fade">
<div v-if="displayLogCall" v-cy-name="'log-call-form'" class="ba br3 mb3 pa3 b--black-40">
<div class="dt dt--fixed pb3 mb3 mb0-ns">
<!-- WHEN -->
<div class="dtc pr2">
<p class="mb2 b">
{{ $t('people.modal_call_exact_date') }}
</p>
<div class="di mr3">
<div class="dib">
<form-date
v-model="newCall.called_at"
:default-date="todayDate"
:locale="locale"
/>
</div>
</div>
</div>
<!-- WHO CALLED -->
<div class="dtc">
<p class="mb2 b">
{{ $t('people.modal_call_who_called') }}
</p>
<div class="dt">
<div class="dt-row">
<form-radio
v-model="newCall.contact_called"
:name="'contact_called'"
:value="false"
:iclass="'mr1'"
:dclass="'dtc mr3'"
>
{{ $t('people.call_you_called') }}
</form-radio>
<form-radio
v-model="newCall.contact_called"
:name="'contact_called'"
:value="true"
:iclass="'mr1'"
:dclass="'dtc mr3'"
>
{{ $t('people.call_he_called', { name : name }) }}
</form-radio>
</div>
</div>
</div>
</div>
<!-- CONTENT -->
<div>
<form-textarea
v-model="newCall.content"
:required="true"
:label="$t('people.modal_call_comment')"
:rows="4"
:placeholder="$t('people.conversation_add_content')"
/>
<p class="f6">
{{ $t('app.markdown_description') }} <a href="https://guides.github.com/features/mastering-markdown/" target="_blank" rel="noopener noreferrer">
{{ $t('app.markdown_link') }}
</a>
</p>
</div>
<!-- EMOTIONS -->
<div class="bb b--gray-monica pb3">
<label class="b">
{{ $t('people.modal_call_emotion') }}
</label>
<emotion class="pv2" @update="updateEmotionsList" />
</div>
<!-- ACTIONS -->
<div class="pt3">
<div class="flex-ns justify-between">
<div class="">
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" href="" @click.prevent="displayLogCall = false; resetFields()">
{{ $t('app.cancel') }}
</a>
</div>
<div class="">
<button v-cy-name="'save-call-button'" class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click.prevent="store()">
{{ $t('app.add') }}
</button>
</div>
</div>
</div>
</div>
</transition>
<!-- LIST OF CALLS -->
<div v-cy-name="'calls-body'" v-cy-items="calls.map(c => c.id)">
<div v-for="call in calls" :key="call.id" v-cy-name="'call-body-'+call.id" class="ba br2 b--black-10 br--top w-100 mb2">
<div v-show="editCallId !== call.id" class="pa2">
<span v-if="!call.content">
{{ $t('people.call_blank_desc', { name: call.contact.first_name }) }}
</span>
<span v-if="call.content" dir="auto" class="markdown" v-html="compiledMarkdown(call.content)"></span>
</div>
<!-- INLINE UPDATE DIV -->
<div v-show="editCallId === call.id" class="pa2">
<div>
<form-textarea
v-model="editCall.content"
:label="$t('people.modal_call_comment')"
:rows="4"
iclass="br2 f5 w-100 ba b--black-40 pa2 outline-0"
@contentChange="updateEditCallContent($event)"
/>
<p class="f6">
{{ $t('app.markdown_description') }}
</p>
</div>
<!-- WHO CALLED -->
<div class="pb3 mb3 mb0-ns">
<p class="mb2">
{{ $t('people.modal_call_who_called') }}
</p>
<div class="di mr3">
<input :id="'you' + call.id" v-model="editCall.contact_called" type="radio" class="mr1" :name="'contact_called' + call.id"
:value="false"
/>
<p class="f6">
{{ $t('app.markdown_description') }}
</p>
</div>
<!-- WHO CALLED -->
<div class="pb3 mb3 mb0-ns">
<p class="mb2">
{{ $t('people.modal_call_who_called') }}
</p>
<div class="di mr3">
<input :id="'you' + call.id" v-model="editCall.contact_called" type="radio" class="mr1" :name="'contact_called' + call.id"
:value="false"
/>
<label :for="'you' + call.id" class="pointer">
{{ $t('people.call_you_called') }}
</label>
</div>
<div class="di mr3">
<input :id="'contact' + call.id" v-model="editCall.contact_called" type="radio" class="mr1" :name="'contact_called' + call.id"
:value="true"
/>
<label :for="'contact' + call.id" class="pointer">
{{ $t('people.call_he_called', { name : name }) }}
</label>
</div>
</div>
<!-- EMOTIONS -->
<div class="bb b--gray-monica pb3 mb3">
<label class="b">
{{ $t('people.modal_call_emotion') }}
</label>
<emotion class="pv2" :initial-emotions="call.emotions" @update="updateEmotionsList" />
</div>
<!-- ACTIONS -->
<div class="">
<div class="flex-ns justify-between">
<div class="">
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" href="" @click.prevent="editCallId = 0">
{{ $t('app.cancel') }}
</a>
</div>
<div class="">
<button class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click.prevent="update()">
{{ $t('app.update') }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- ADDITIONAL INFORMATION -->
<div class="pa2 cf bt b--black-10 br--bottom f7 lh-copy">
<div class="w-70" :class="[ dirltr ? 'fl' : 'fr' ]">
<span :class="[ dirltr ? 'mr3' : 'ml3' ]">
{{ call.called_at | moment }}
</span>
<span :class="[ dirltr ? 'mr3' : 'ml3' ]">
{{ call.contact_called ? $t('people.call_he_called', { name : name }) : $t('people.call_you_called') }}
</span>
<!-- EMOTION LIST -->
<span v-if="call.emotions.length !== 0">
<span :class="[ dirltr ? 'mr2' : 'ml2' ]">
{{ $t('people.call_emotions') }}
</span>
<ul class="di">
<li v-for="emotion in call.emotions" :key="emotion.id" class="di">
{{ $t('app.emotion_' + emotion.name) }}
</li>
</ul>
</span>
</div>
<div :class="[ dirltr ? 'fl tr' : 'fr tl' ]" class="w-30">
<a :class="[ dirltr ? 'mr2' : 'ml2' ]" class="pointer " href="" @click.prevent="showEditBox(call)">
{{ $t('app.update') }}
</a>
<a v-show="destroyCallId !== call.id" v-cy-name="'delete-call-button-'+call.id" class="pointer" href="" @click.prevent="showDestroyCall(call)">
{{ $t('app.delete') }}
</a>
<ul v-show="destroyCallId === call.id" class="di">
<li class="di">
<a class="pointer mr1" href="" @click.prevent="destroyCallId = 0">
{{ $t('app.cancel') }}
</a>
</li>
<li class="di">
<a v-cy-name="'delete-call-confirm-button-'+call.id" class="pointer red" href="" @click.prevent="destroyCall(call)">
{{ $t('app.delete_confirm') }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import Emotion from '../Emotion.vue';
export default {
components: {
Emotion,
},
filters: {
moment: function (date) {
return moment.utc(date).format('LL');
}
},
props: {
hash: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
},
data() {
return {
calls: [],
displayLogCall: false,
todayDate: '',
editCallId: 0,
destroyCallId: 0,
chosenEmotions: [],
newCall: {
content: '',
called_at: '',
contact_called: false,
emotions: [],
},
editCall: {
content: '',
contact_called: false,
emotions: [],
}
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
locale() {
return this.$root.locale;
}
},
mounted() {
this.prepareComponent(this.hash);
},
methods: {
prepareComponent(hash) {
this.getCalls();
this.todayDate = moment().format('YYYY-MM-DD');
this.newCall.called_at = this.todayDate;
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
},
resetFields() {
this.newCall.content = '';
this.newCall.called_at = this.todayDate;
},
getCalls() {
axios.get('people/' + this.hash + '/calls')
.then(response => {
this.calls = response.data.data;
});
},
store() {
axios.post('people/' + this.hash + '/calls', this.newCall)
.then(response => {
this.getCalls();
this.resetFields();
this.displayLogCall = false;
this.chosenEmotions = [];
this.updateLastCalled();
this.$notify({
group: 'main',
title: this.$t('people.calls_add_success'),
text: '',
type: 'success'
});
});
},
update() {
axios.put('people/' + this.hash + '/calls/' + this.editCallId, this.editCall)
.then(response => {
this.getCalls();
this.editCallId = 0;
this.chosenEmotions = [];
this.updateLastCalled();
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
updateLastCalled() {
this.$parent.$refs.lastCalledAttribute.getLastCalled();
},
showEditBox(call) {
this.editCallId = call.id;
this.editCall.content = call.content;
this.editCall.contact_called = call.contact_called;
this.editCall.called_at = moment.utc(call.called_at).format('YYYY-MM-DD');
},
updateDate(updatedContent) {
this.newCall.called_at = updatedContent;
},
showDestroyCall(call) {
this.destroyCallId = call.id;
},
destroyCall(call) {
axios.delete('people/' + this.hash + '/calls/' + this.destroyCallId)
.then(response => {
this.calls.splice(this.calls.indexOf(call), 1);
this.updateLastCalled();
});
},
updateEmotionsList: function(emotions) {
this.chosenEmotions = emotions;
this.newCall.emotions = [];
this.editCall.emotions = [];
// filter the list of emotions to populate a new array
// containing only the emotion ids and not the entire objetcs
for (let i = 0; i < this.chosenEmotions.length; i++) {
this.newCall.emotions.push(this.chosenEmotions[i].id);
this.editCall.emotions.push(this.chosenEmotions[i].id);
}
}
}
};
</script>

View File

@@ -0,0 +1,114 @@
<style scoped>
.me {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid #fff;
top: 120px;
right: -10px;
}
.other {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right:10px solid #fff;
top: 120px;
left: -10px;
}
.conversation-block {
background-color: #D8E2E7;
}
</style>
<template>
<div class="pa4-ns ph3 pv2 mb3 mb0-ns bb b--gray-monica">
<p class="mb2 b">
{{ $t('people.conversation_add_what_was_said') }}
</p>
<div class="pa3 ba b--gray-monica br3 conversation-block">
<div v-for="message in messages" :key="message.uid" class="relative">
<div :class="message.author + ' absolute'"></div>
<message
v-model="message.content"
:class="{ 'mb3 ml5': message.author === 'me', 'mb3 mr5': message.author === 'other' }"
:author="message.author"
:uid="message.uid"
:participant-name="participantName"
:display-trash="displayTrash"
@updateAuthor="updateAuthor($event, message)"
@deleteMessage="deleteMessage($event, message)"
/>
</div>
<p class="tc mb0">
<a class="btn btn-secondary pointer" href="" @click.prevent="addMessage">
{{ $t('people.conversation_add_another') }}
</a>
</p>
<input type="hidden" name="messages" :value="messages.map(a => a.uid)" />
</div>
</div>
</template>
<script>
export default {
props: {
participantName: {
type: String,
default: '',
},
existingMessages: {
type: Array,
default: () => [],
},
},
data() {
return {
messages: [],
uid: 1,
displayTrash: false,
};
},
mounted() {
if (this.existingMessages.length > 0) {
this.messages = this.existingMessages;
this.uid = this.messages[this.messages.length - 1].uid + 1;
} else {
var me = this;
setTimeout(function () {
me.addMessage();
}, 10);
}
},
methods: {
addMessage() {
this.messages.push({
uid: this.uid++,
content: '',
author: 'me'
});
if (this.messages.length > 1) {
this.displayTrash = true;
}
},
updateAuthor(updatedAuthor, message) {
message.author = updatedAuthor;
},
deleteMessage(uid, message) {
this.messages.splice(this.messages.indexOf(this.messages.find(item => item.uid === uid)), 1);
if (this.messages.length <= 1) {
this.displayTrash = false;
}
},
}
};
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div>
<vue-good-table
:columns="columns"
:rows="conversations"
style-class="vgt-table"
:sort-options="{
enabled: false
}"
:pagination-options="{
enabled: true
}"
@on-row-click="onRowClick"
>
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field === 'message_count'">
<span>
{{ props.row.message_count }}
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.29412 14.6014V12.8182H1.69853C0.772058 12.8182 0 12.0839 0 11.2028V6.67133C0 5.79021 0.772058 5.05594 1.69853 5.05594H5.60294C5.82353 5.05594 6.02206 5.22378 6.02206 5.45455C6.02206 5.68531 5.84559 5.85315 5.60294 5.85315H1.69853C1.21323 5.85315 0.816176 6.23077 0.816176 6.69231V11.2238C0.816176 11.6853 1.21323 12.0629 1.69853 12.0629H2.71324C2.93382 12.0629 3.13235 12.2308 3.13235 12.4615V13.6993L4.80882 12.1678C4.875 12.1049 4.9853 12.0629 5.09559 12.0629H9.52941C10.0147 12.0629 10.4118 11.6853 10.4118 11.2238V8.91608C10.4118 8.70629 10.5882 8.51748 10.8309 8.51748C11.0515 8.51748 11.25 8.68532 11.25 8.91608V11.2238C11.25 12.1049 10.4779 12.8392 9.55147 12.8392H5.25L3 14.8951C2.91177 14.958 2.82353 15 2.71324 15C2.66912 15 2.60294 15 2.55882 14.958C2.40441 14.8951 2.29412 14.7482 2.29412 14.6014ZM12.7059 7.84615H8.40441C7.45588 7.84615 6.68382 7.11188 6.68382 6.20979V1.63636C6.68382 0.734263 7.45588 0 8.40441 0H16.2794C17.2279 0 18 0.734263 18 1.63636V6.20979C18 7.11188 17.2279 7.84615 16.2794 7.84615H15.6838V9.62937C15.6838 9.7972 15.5956 9.92307 15.4412 9.98601C15.3971 10.007 15.3309 10.028 15.2868 10.028C15.1765 10.028 15.0882 9.98601 15 9.92307L12.7059 7.84615ZM8.40441 7.06993H12.8603C12.9706 7.06993 13.0588 7.11188 13.1471 7.17482L14.8456 8.72727V7.46853C14.8456 7.25874 15.0221 7.06993 15.2647 7.06993H16.2794C16.7647 7.06993 17.1618 6.6923 17.1618 6.23077V1.65734C17.1618 1.1958 16.7647 0.818181 16.2794 0.818181H8.40441C7.91912 0.818181 7.52206 1.1958 7.52206 1.65734V6.23077C7.52206 6.6923 7.91912 7.06993 8.40441 7.06993ZM12.3529 3.54545C12.0441 3.54545 11.7794 3.7972 11.7794 4.09091C11.7794 4.38462 12.0441 4.63636 12.3529 4.63636C12.6618 4.63636 12.9265 4.38462 12.9265 4.09091C12.9265 3.7972 12.6618 3.54545 12.3529 3.54545ZM14.4926 4.63636C14.8015 4.63636 15.0662 4.38462 15.0662 4.09091C15.0662 3.7972 14.8015 3.54545 14.4926 3.54545C14.1838 3.54545 13.9191 3.7972 13.9191 4.09091C13.9191 4.38462 14.1838 4.63636 14.4926 4.63636ZM10.1912 3.54545C9.88235 3.54545 9.61765 3.7972 9.61765 4.09091C9.61765 4.38462 9.88235 4.63636 10.1912 4.63636C10.5 4.63636 10.7647 4.38462 10.7647 4.09091C10.7647 3.7972 10.5 3.54545 10.1912 3.54545Z" fill="#67718A" />
</svg>
</span>
</span>
<span v-else>
{{ props.formattedRow[props.column.field] }}
</span>
</template>
</vue-good-table>
</div>
</template>
<script>
import { VueGoodTable } from 'vue-good-table';
export default {
components: {
VueGoodTable
},
props: {
hash: {
type: String,
default: '',
},
},
data() {
return {
conversations: [],
columns: [
{
label: this.$t('app.date'),
field: 'happened_at',
tdClass: 'vgt-table-date',
},
{
label: this.$t('app.type'),
field: 'contact_field_type',
width: '110px',
},
{
label: this.$t('people.conversation_list_table_messages'),
field: 'message_count',
width: '110px',
},
{
label: this.$t('people.conversation_list_table_content'),
field: 'content',
}
],
};
},
mounted() {
this.prepareComponent(this.hash);
},
methods: {
prepareComponent(hash) {
axios.get('people/' + hash + '/conversations')
.then(response => {
this.conversations = response.data;
});
},
onRowClick(params) {
window.location.href = params.row.route;
}
}
};
</script>

View File

@@ -0,0 +1,139 @@
<style scoped>
.delete-message-l {
top: 9px;
right: 16px;
}
.delete-message-r {
top: 9px;
left: 16px;
}
.btn-secondary {
font-size: 12px;
padding: 4px 10px;
}
</style>
<template>
<div class="bg-white pa3 relative br3">
<!-- TRASH CAN -->
<span v-if="displayTrash" :class="[dirltr ? 'absolute delete-message-l' : 'absolute delete-message-r']">
<a class="pointer btn btn-secondary" href="" @click.prevent="deleteMessage">
<svg class="mr1 relative" style="top: 1px;" width="9" height="11" viewBox="0 0 9 11"
fill="none" xmlns="http://www.w3.org/2000/svg"
>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.27273 0.714286H5.81818C5.81818 0.321429 5.49091 0 5.09091 0H2.90909C2.50909 0 2.18182 0.321429 2.18182 0.714286H0.727273C0.327273 0.714286 0 1.03571 0 1.42857V2.14286C0 2.53571 0.327273 2.85714 0.727273 2.85714V9.28571C0.727273 9.67857 1.05455 10 1.45455 10H6.54545C6.94545 10 7.27273 9.67857 7.27273 9.28571V2.85714C7.67273 2.85714 8 2.53571 8 2.14286V1.42857C8 1.03571 7.67273 0.714286 7.27273 0.714286ZM6.54545 9.28571H1.45455V2.85714H2.18182V8.57143H2.90909V2.85714H3.63636V8.57143H4.36364V2.85714H5.09091V8.57143H5.81818V2.85714H6.54545V9.28571ZM7.27273 2.14286H0.727273V1.42857H7.27273V2.14286Z" fill="#626262" />
</svg>
{{ $t('app.delete') }}
</a>
</span>
<!-- AUTHOR -->
<div class="mb3">
<span class="di mr3">
{{ $t('people.conversation_add_who_wrote') }}
</span>
<div class="di mr3">
<input :id="'other_' + uid" v-model="updatedAuthor" class="pointer" type="radio" :name="'who_wrote_' + uid"
value="other" :checked="updatedAuthor === 'other'" @click="updateAuthor('other')"
/>
<label :for="'other_' + uid" class="pointer">
{{ participantName }}
</label>
</div>
<div class="di">
<input :id="'me_' + uid" v-model="updatedAuthor" class="pointer" type="radio" :name="'who_wrote_' + uid"
value="me" :checked="updatedAuthor === 'me'" @click="updateAuthor('me')"
/>
<label :for="'me_' + uid" class="pointer">
{{ $t('people.conversation_add_you') }}
</label>
</div>
</div>
<!-- ACTUAL COMMENT -->
<form-textarea
:id="'content_' + uid"
v-model="buffer"
:required="true"
:no-label="true"
:rows="4"
:placeholder="$t('people.conversation_add_content')"
/>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
uid: {
type: Number,
default: 0,
},
id: {
type: String,
default: '',
},
participantName: {
type: String,
default: '',
},
author: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
required: {
type: Boolean,
default: true,
},
displayTrash: {
type: Boolean,
default: true,
},
},
data() {
return {
buffer: '',
updatedAuthor: this.author,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
watch: {
value(newValue) {
this.buffer = newValue;
}
},
mounted() {
this.buffer = this.value;
},
methods: {
deleteMessage() {
this.$emit('deleteMessage', this.uid);
},
updateAuthor(newAuthor) {
this.updatedOther = newAuthor;
this.$emit('updateAuthor', newAuthor);
},
}
};
</script>

View File

@@ -0,0 +1,345 @@
<style scoped>
.document-upload-zone {
background: #fff;
border: 1px solid #d6d6d6;
border-style: dashed;
}
.document-type {
width: 60px;
}
.document-type-icon {
background: #ADF4FE;
border-radius: 13px;
font-size: 11px;
padding: 5px 8px;
}
.document-date {
width: 110px;
}
.document-action {
width: 50px;
}
.document-action-wrapper {
background: #fff;
opacity: 0.8;
border-radius: 3px;
line-height: 18px;
padding: 2px 5px;
}
.document-action-wrapper:hover {
box-shadow: 1px 0px 1px rgba(43, 45, 80, 0.16), -1px 1px 1px rgba(43, 45, 80, 0.16), 0px 1px 4px rgba(43, 45, 80, 0.18);
}
.document-action-menu {
border-radius: 3px;
box-shadow: 1px 0px 1px rgba(43, 45, 80, 0.16), -1px 1px 1px rgba(43, 45, 80, 0.16), 0px 1px 4px rgba(43, 45, 80, 0.18);
right: 10px;
top: 34px;
}
.document-action-menu-item.delete {
color: #CB4066;
}
.document-action-menu-item.delete:hover {
color: #fff;
}
progress {
-webkit-appearance: none;
border: none;
height: 8px;
margin-bottom: 3px;
width: 60%;
}
progress::-webkit-progress-bar {
background: #e2e7ee;
border-radius: 3px;
}
progress::-webkit-progress-value {
border-radius: 3px;
box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
background-color: #329FF1;
}
</style>
<template>
<div>
<div class="">
<h3>
📄 {{ $t('people.document_list_title') }}
<span v-show="reachLimit === 'false'" class="fr relative" style="top: -7px;">
<a v-if="displayUploadZone === false && displayUploadError === false && displayUploadProgress === false" class="btn edit-information" html="" @click.prevent="displayUploadZone = true">
{{ $t('people.document_list_cta') }}
</a>
<a v-if="displayUploadZone || displayUploadError || displayUploadProgress" class="btn edit-information" href="" @click.prevent="displayUploadZone = false; displayUploadError = false; displayUploadProgress = false">
{{ $t('app.cancel') }}
</a>
</span>
</h3>
</div>
<p v-show="reachLimit === 'true'">
{{ $t('settings.storage_upgrade_notice') }}
</p>
<!-- EMPTY STATE -->
<div v-if="displayUploadZone === false && displayUploadError === false && displayUploadProgress === false && documents.length === 0" class="ltr w-100 pt2">
<div class="section-blank">
<h3 class="mb0">
{{ $t('people.document_list_blank_desc') }}
</h3>
</div>
</div>
<!-- FIRST STEP OF DOCUMENT UPLOAD -->
<transition name="fade">
<div v-if="displayUploadZone" class="ba br3 document-upload-zone mb3 pa3">
<div class="tc">
<svg width="120" height="150" viewBox="0 0 120 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.2012 35.9138V101.562H92.7999V18.4375H44.6762L27.2012 35.9138ZM44.1274 21.6388V35.365H30.4024L44.1274 21.6388ZM46.0024 20.3125H90.9249V99.6875H29.0762V37.24H46.0024V20.3125Z" fill="#868E99" />
<path d="M63.9413 42.7338H36.3013V44.6088H63.9413V42.7338Z" fill="#868E99" />
<path d="M63.9413 52.6112H36.3013V54.4862H63.9413V52.6112Z" fill="#868E99" />
<path d="M83.6988 62.4888H36.3013V64.3638H83.6988V62.4888Z" fill="#868E99" />
<path d="M83.6988 72.3676H36.3013V74.2426H83.6988V72.3676Z" fill="#868E99" />
<path d="M83.6988 82.245H36.3013V84.12H83.6988V82.245Z" fill="#868E99" />
<path d="M83.6988 92.1226H36.3013V93.9976H83.6988V92.1226Z" fill="#868E99" />
<path d="M84.6362 41.585H67.8774V58.3425H84.6362V41.585ZM82.7612 56.4675H69.7524V43.46H82.7612V56.4675Z" fill="#868E99" />
<path d="M35.2864 107.656C33.6551 107.656 32.3276 108.984 32.3276 110.615C32.3276 112.246 33.6551 113.574 35.2864 113.574C36.9176 113.574 38.2451 112.246 38.2451 110.615C38.2451 108.984 36.9176 107.656 35.2864 107.656ZM35.2864 112.636C34.1714 112.636 33.2651 111.729 33.2651 110.615C33.2651 109.501 34.1726 108.594 35.2864 108.594C36.4001 108.594 37.3076 109.501 37.3076 110.615C37.3076 111.729 36.4001 112.636 35.2864 112.636Z" fill="#868E99" />
<path d="M10.6401 41.265L13.3251 42.6762L12.8126 39.6862L14.9851 37.5687L11.9839 37.1325L10.6401 34.4125L9.29764 37.1325L6.29639 37.5687L8.46889 39.6862L7.95639 42.6762L10.6401 41.265ZM8.31014 38.2225L9.92014 37.9887L10.6401 36.53L11.3601 37.9887L12.9701 38.2225L11.8051 39.3587L12.0801 40.9625L10.6401 40.2062L9.20014 40.9625L9.47514 39.3587L8.31014 38.2225Z" fill="#868E99" />
<path d="M36.3237 10.1125L35.365 8.17004L34.405 10.1125L32.2612 10.425L33.8125 11.9375L33.4462 14.0725L35.3637 13.065L37.2812 14.0725L36.9162 11.9375L38.4675 10.425L36.3237 10.1125ZM36.0375 12.36L35.365 12.0063L34.6925 12.36L34.8212 11.61L34.275 11.0788L35.0275 10.97L35.3637 10.2888L35.7 10.97L36.4525 11.0788L35.9075 11.61L36.0375 12.36Z" fill="#868E99" />
<path d="M108.859 87.615L110.776 88.6225L110.41 86.4875L111.961 84.975L109.818 84.6625L108.859 82.72L107.9 84.6625L105.756 84.975L107.308 86.4875L106.941 88.6225L108.859 87.615ZM108.523 85.52L108.859 84.8387L109.195 85.52L109.948 85.6287L109.403 86.16L109.531 86.91L108.859 86.5562L108.186 86.91L108.315 86.16L107.77 85.6287L108.523 85.52Z" fill="#868E99" />
<path d="M108.859 17.1625L107.618 20.9837H103.6L106.849 23.345L105.609 27.165L108.859 24.8037L112.109 27.165L110.868 23.345L114.116 20.9837H110.1L108.859 17.1625ZM109.765 22.9875L110.325 24.71L108.859 23.645L107.391 24.7112L107.951 22.9875L106.485 21.9212H108.299L108.859 20.1975L109.419 21.9212H111.233L109.765 22.9875Z" fill="#868E99" />
<path d="M98.9138 6.42749C97.2826 6.42749 95.9551 7.75499 95.9551 9.38624C95.9551 11.0175 97.2826 12.345 98.9138 12.345C100.545 12.345 101.873 11.0175 101.873 9.38624C101.873 7.75499 100.545 6.42749 98.9138 6.42749ZM98.9138 11.4075C97.7988 11.4075 96.8926 10.5 96.8926 9.38624C96.8926 8.27249 97.8001 7.36499 98.9138 7.36499C100.028 7.36499 100.935 8.27249 100.935 9.38624C100.935 10.5 100.028 11.4075 98.9138 11.4075Z" fill="#868E99" />
<path d="M110.779 58.8438C109.461 58.8438 108.39 59.915 108.39 61.2325C108.39 62.55 109.461 63.6213 110.779 63.6213C112.096 63.6213 113.168 62.55 113.168 61.2325C113.168 59.915 112.096 58.8438 110.779 58.8438ZM110.779 62.6837C109.979 62.6837 109.328 62.0325 109.328 61.2325C109.328 60.4325 109.979 59.7813 110.779 59.7813C111.579 59.7813 112.23 60.4325 112.23 61.2325C112.23 62.0325 111.579 62.6837 110.779 62.6837Z" fill="#868E99" />
<path d="M18.6973 18.3362C16.9836 18.3362 15.5898 19.7299 15.5898 21.4437C15.5898 23.1574 16.9836 24.5512 18.6973 24.5512C20.4111 24.5512 21.8048 23.1574 21.8048 21.4437C21.8048 19.7299 20.4111 18.3362 18.6973 18.3362ZM18.6973 23.6137C17.5011 23.6137 16.5273 22.6399 16.5273 21.4437C16.5273 20.2474 17.5011 19.2737 18.6973 19.2737C19.8936 19.2737 20.8673 20.2474 20.8673 21.4437C20.8673 22.6399 19.8936 23.6137 18.6973 23.6137Z" fill="#868E99" />
<path d="M27.4414 110.794C27.8502 110.898 28.2652 110.99 28.6839 111.074L28.8677 110.155C28.4639 110.074 28.0652 109.984 27.6702 109.885L27.4414 110.794Z" fill="#868E99" />
<path d="M9.64111 94.6313C9.91986 95.4101 10.2424 96.1876 10.5999 96.9413L11.4474 96.5413C11.1036 95.8138 10.7936 95.0663 10.5249 94.3151L9.64111 94.6313Z" fill="#868E99" />
<path d="M11.7773 99.1475C12.2048 99.86 12.6711 100.56 13.1661 101.229L13.9198 100.671C13.4448 100.028 12.9936 99.3525 12.5823 98.6663L11.7773 99.1475Z" fill="#868E99" />
<path d="M22.7339 109.106C23.4851 109.463 24.2639 109.785 25.0464 110.066L25.3626 109.184C24.6076 108.914 23.8589 108.603 23.1351 108.26L22.7339 109.106Z" fill="#868E99" />
<path d="M14.75 103.164C15.305 103.78 15.8988 104.378 16.5163 104.938L17.1475 104.244C16.5538 103.704 15.9813 103.129 15.4463 102.536L14.75 103.164Z" fill="#868E99" />
<path d="M9.08887 86.0649C9.08887 85.6587 9.09887 85.2512 9.11887 84.8412L8.18262 84.7949C8.16137 85.2199 8.15137 85.6424 8.15137 86.0649C8.15137 86.4774 8.16137 86.8887 8.18137 87.2974L9.11762 87.2524C9.09887 86.8574 9.08887 86.4612 9.08887 86.0649Z" fill="#868E99" />
<path d="M9.35279 89.6475L8.42529 89.785C8.54779 90.6062 8.71154 91.4312 8.91404 92.2375L9.82404 92.0087C9.62904 91.2325 9.47029 90.4387 9.35279 89.6475Z" fill="#868E99" />
<path d="M18.4473 106.53C19.116 107.027 19.816 107.496 20.526 107.924L21.0098 107.12C20.326 106.709 19.6523 106.257 19.0073 105.777L18.4473 106.53Z" fill="#868E99" />
<path d="M9.36012 82.425C9.42012 82.0225 9.49012 81.6187 9.57012 81.2162L8.65137 81.0325C8.56762 81.4512 8.49512 81.8687 8.43262 82.2875L9.36012 82.425Z" fill="#868E99" />
<path d="M111.323 96.2138L112.257 96.2988C112.295 95.8763 112.319 95.4488 112.328 95.0176L111.39 94.9976C111.382 95.4076 111.359 95.8126 111.323 96.2138Z" fill="#868E99" />
<path d="M110.009 101.347L110.869 101.721C111.241 100.865 111.546 99.9725 111.779 99.0675L110.87 98.8337C110.651 99.6912 110.361 100.537 110.009 101.347Z" fill="#868E99" />
<path d="M107.156 105.81L107.858 106.433C108.48 105.733 109.049 104.98 109.55 104.196L108.76 103.691C108.284 104.434 107.745 105.146 107.156 105.81Z" fill="#868E99" />
<path d="M98.1226 111.079L98.3176 111.995C99.2288 111.801 100.134 111.531 101.005 111.192L100.666 110.319C99.8413 110.64 98.9851 110.895 98.1226 111.079Z" fill="#868E99" />
<path d="M103.059 109.166L103.531 109.975C104.337 109.505 105.111 108.966 105.834 108.374L105.239 107.649C104.555 108.211 103.821 108.721 103.059 109.166Z" fill="#868E99" />
<path d="M94.2637 111.444L94.2437 112.381C94.3649 112.384 94.4862 112.385 94.6074 112.385C94.9162 112.385 95.2224 112.378 95.5287 112.361L95.4799 111.425C95.0774 111.446 94.6724 111.455 94.2637 111.444Z" fill="#868E99" />
<path d="M111.248 45.4712H110.31V47.9712H111.248V45.4712Z" fill="#868E99" />
<path d="M111.248 40.4712H110.31V42.9712H111.248V40.4712Z" fill="#868E99" />
<path d="M111.248 35.4712H110.31V37.9712H111.248V35.4712Z" fill="#868E99" />
<path d="M111.248 50.4712H110.31V52.9712H111.248V50.4712Z" fill="#868E99" />
</svg>
</div>
<div class="tc dib w-100 relative">
<button class="btn">
{{ $t('people.document_upload_zone_cta') }}
</button>
<input id="file" ref="file" type="file" class="absolute o-0 w-100 h-100 pointer" style="left:0;"
@change="handleFileUpload()"
/>
</div>
</div>
</transition>
<!-- LAST STEP OF DOCUMENT UPLOAD -->
<div v-if="displayUploadProgress" class="ba br3 document-upload-zone mb3 pa3">
<p class="tc mb1">
{{ $t('people.document_upload_zone_progress') }}
</p>
<div class="tc mb1">
<progress max="100" :value.prop="uploadPercentage"></progress>
</div>
<p class="tc f6 mb0">
{{ uploadPercentage }}% uploaded
</p>
</div>
<!-- ERROR STEP WHEN UPLOADING A DOCUMENT -->
<transition name="fade">
<div v-if="displayUploadError" class="ba br3 document-upload-zone mb3 pa3">
<div class="tc">
<svg width="120" height="150" viewBox="0 0 120 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.2012 35.9138V101.562H92.7999V18.4375H44.6762L27.2012 35.9138ZM44.1274 21.6388V35.365H30.4024L44.1274 21.6388ZM46.0024 20.3125H90.9249V99.6875H29.0762V37.24H46.0024V20.3125Z" fill="#868E99" />
<path d="M83.6988 72.3676H36.3013V74.2426H83.6988V72.3676Z" fill="#868E99" />
<path d="M83.6988 82.245H36.3013V84.12H83.6988V82.245Z" fill="#868E99" />
<path d="M83.6988 92.1226H36.3013V93.9976H83.6988V92.1226Z" fill="#868E99" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.5862 46.875C79.367 46.875 80 46.2314 80 45.4375C80 44.6436 79.367 44 78.5862 44H68.6897C67.9088 44 67.2759 44.6436 67.2759 45.4375C67.2759 47.8192 69.1748 49.75 71.5172 49.75C73.364 49.75 74.935 48.5499 75.5173 46.875H78.5862ZM40.4138 46.875C39.633 46.875 39 46.2314 39 45.4375C39 44.6436 39.633 44 40.4138 44H50.3103C51.0912 44 51.7241 44.6436 51.7241 45.4375C51.7241 47.8192 49.8252 49.75 47.4828 49.75C45.636 49.75 44.065 48.5499 43.4827 46.875H40.4138ZM55.7524 66.3213C55.888 66.0995 56.2309 65.69 56.7753 65.2677C57.698 64.552 58.8279 64.1248 60.2069 64.1248C61.5859 64.1248 62.7158 64.552 63.6385 65.2677C64.1829 65.69 64.5258 66.0995 64.6614 66.3213C65.0736 66.9955 65.9454 67.2023 66.6085 66.7831C67.2716 66.364 67.475 65.4776 67.0628 64.8034C66.7589 64.3064 66.199 63.6378 65.3535 62.9819C63.9596 61.9008 62.2381 61.2499 60.2069 61.2499C58.1757 61.2499 56.4542 61.9008 55.0603 62.9819C54.2148 63.6378 53.6549 64.3064 53.351 64.8034C52.9388 65.4776 53.1422 66.364 53.8053 66.7831C54.4684 67.2023 55.3402 66.9955 55.7524 66.3213Z" fill="#868E99" />
</svg>
</div>
<p class="tc mb3">
{{ $t('people.document_upload_zone_error') }}
</p>
<p class="tc">
<input id="file" ref="file" type="file" @change="handleFileUpload()" />
</p>
</div>
</transition>
<!-- LIST OF DOCUMENTS -->
<div class="table">
<div v-for="document in documents" :key="document.id" class="table-row">
<div class="table-cell document-date f6">
{{ formatTime(document.created_at) }}
</div>
<div class="table-cell document-type tc">
<span class="document-type-icon">
{{ document.type }}
</span>
</div>
<div class="table-cell">
{{ document.original_filename }}
</div>
<div class="table-cell document-action relative">
<div class="document-action-wrapper pointer" @click="toggleActionsModal(document.id)">
<svg class="document-action-button" width="24" height="6" viewBox="0 0 24 6" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="4.5" cy="2.5" r="2.5" fill="#505473" fill-opacity="0.86" />
<circle cx="11.5" cy="2.5" r="2.5" fill="#505473" fill-opacity="0.86" />
<circle cx="18.5" cy="2.5" r="2.5" fill="#505473" fill-opacity="0.86" />
</svg>
</div>
<ul v-if="modalToDisplay === document.id" class="absolute bg-white z-max pv1 document-action-menu">
<li class="tc">
<a class="pv2 pointer ph3 inline-flex items-center w-100 no-underline document-action-menu-item" :href="document.link" target="_blank" @click="downloadDocument(document)">
{{ $t('app.download') }}
</a>
</li>
<li class="tc">
<a class="pv2 pointer ph3 inline-flex items-center no-underline w-100 document-action-menu-item delete" href="" @click.prevent="deleteDocument(document)">
{{ $t('app.delete') }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
reachLimit: {
type: String,
default: '',
},
},
data() {
return {
documents: [],
displayUploadZone: false,
displayUploadProgress: false,
displayUploadError: false,
file: '',
uploadPercentage: 0,
modalToDisplay: null,
};
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getDocuments();
},
getDocuments() {
axios.get('people/' + this.hash + '/documents')
.then(response => {
this.documents = response.data.data;
});
},
showUploadZone() {
this.displayUploadZone = true;
},
toggleActionsModal(id) {
if (this.modalToDisplay === id) {
this.modalToDisplay = null;
} else {
this.modalToDisplay = id;
}
},
handleFileUpload(){
this.file = this.$refs.file.files[0];
this.submitFile();
},
formatTime(dateAsString) {
var moment = require('moment-timezone');
moment.locale(this._i18n.locale);
var date = moment(dateAsString);
return date.format('ll');
},
submitFile(){
this.displayUploadZone = false;
this.displayUploadProgress = true;
const formData = new FormData();
formData.append('document', this.file);
axios.post( 'people/' + this.hash + '/documents',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: function( progressEvent ) {
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
}.bind(this)
}
).then(response => {
this.displayUploadProgress = false;
this.documents.push(response.data);
})
.catch(error => {
this.displayUploadProgress = false;
this.file = null;
this.displayUploadError = true;
});
},
downloadDocument(doc) {
window.open(doc.link, '_blank');
// Close the modal menu
this.modalToDisplay = null;
},
deleteDocument(document) {
axios.delete( 'people/' + this.hash + '/documents/' + document.id)
.then(response => {
this.documents.splice(this.documents.indexOf(document), 1);
});
},
}
};
</script>

View File

@@ -0,0 +1,460 @@
<style scoped>
.photo {
height: 200px;
}
</style>
<template>
<div>
<!-- Add a gift -->
<transition name="fade">
<div class="ba br3 mb3 pa3 b--black-40">
<div class="pb3 mb3 flex-ns b--gray-monica">
<!-- STATUS -->
<form-radio
:id="'status_idea'"
v-model="newGift.status"
:name="'status'"
:required="true"
:value="'idea'"
:color="'success'"
:full-class="'p-default p-fill p-curve'"
>
{{ $t('people.gifts_add_gift_idea') }}
</form-radio>
<form-radio
:id="'status_offered'"
v-model="newGift.status"
:name="'status'"
:required="true"
:value="'offered'"
:color="'info'"
:full-class="'p-default p-fill p-curve'"
>
{{ $t('people.gifts_add_gift_already_offered') }}
</form-radio>
<form-radio
:id="'status_received'"
v-model="newGift.status"
:name="'status'"
:required="true"
:value="'received'"
:color="'warning'"
:full-class="'p-default p-fill p-curve'"
>
{{ $t('people.gifts_add_gift_received') }}
</form-radio>
</div>
<div class="dt dt--fixed pb3 mb3 mb0-ns bb b--gray-monica">
<!-- NAME -->
<form-input
:id="'name'"
v-model="newGift.name"
:input-type="'text'"
:maxlength="255"
:required="true"
:class="'dtc pr2'"
:title="$t('people.gifts_add_gift_name')"
:validator="$v.newGift.name"
@submit="store"
/>
</div>
<!-- ADDITIONAL FIELDS -->
<div v-show="displayMenu" class="bb b--gray-monica pv3 mb3">
<ul class="list">
<li v-show="!displayComment" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="displayComment = true">{{ $t('people.gifts_add_comment') }}</a>
</li>
<li v-show="!displayUrl" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="displayUrl = true">{{ $t('people.gifts_add_link') }}</a>
</li>
<li v-show="!displayAmount" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="displayAmount = true; newGift.amount = 0;">{{ $t('people.gifts_add_value') }}</a>
</li>
<li v-if="familyContacts.length > 0" v-show="!displayRecipient" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="displayRecipient = true">{{ $t('people.gifts_add_recipient') }}</a>
</li>
<li v-if="!reachLimit" v-show="!displayUpload" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="() => { displayUpload = true; $refs.upload.showUploadZone(); }">{{ $t('people.gifts_add_photo') }}</a>
</li>
<li v-show="!displayDate" class="di pointer" :class="dirltr ? 'mr3' : 'ml3'">
<a href="" @click.prevent="displayDate = true">{{ $t('people.gifts_add_date') }}</a>
</li>
</ul>
</div>
<div v-if="displayComment" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<!-- COMMENT -->
<form-input
:id="'comment'"
v-model="newGift.comment"
:input-type="'text'"
:class="'dtc pr2'"
:title="$t('people.gifts_add_comment')"
@submit="store"
/>
</div>
<div v-if="displayUrl" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<!-- URL -->
<form-input
:id="'url'"
v-model="newGift.url"
:input-type="'text'"
:class="'dtc pr2'"
:title="$t('people.gifts_add_link')"
:placeholder="'https://'"
@submit="store"
/>
</div>
<div v-if="displayDate" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<!-- Date -->
<form-date
:id="'date'"
v-model="newGift.date"
:show-calendar-on-focus="true"
:locale="locale"
:class="[ dirltr ? 'fl dtc pr2' : 'fr dtc pr2' ]"
:label="$t('people.gifts_add_date')"
@submit="store"
/>
</div>
<div v-if="displayAmount" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<!-- AMOUNT -->
<form-input
:id="'amount'"
v-model="newGift.amount"
:input-type="'number'"
:class="'dtc pr2'"
:title="$t('people.gifts_add_value')"
:required="displayAmount"
step=".01"
@submit="store"
/>
</div>
<div v-if="displayRecipient" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<!-- RECIPIENT -->
<form-checkbox
v-model="hasRecipient"
:name="'has_recipient'"
@change="(val) => { if (val) { $refs.recipient.focus() } }"
>
{{ $t('people.gifts_add_someone', {name: ''}) }}
</form-checkbox>
<form-select
ref="recipient"
v-model="newGift.recipient_id"
:label="$t('people.gifts_add_recipient_field')"
:options="familyContacts"
:validator="$v.newGift.recipient_id"
@input="hasRecipient = true"
/>
</div>
<div v-show="displayUpload" class="dt dt--fixed pb3 mb3 bb b--gray-monica">
<span class="mb2 b">
{{ $t('people.gifts_add_photo_title') }}
</span>
<photo-upload
v-show="photos.length === 0"
ref="upload"
:hash="hash"
:contact-id="contactId"
@upload.stop="handlePhoto($event)"
/>
<!-- LIST OF PHOTO -->
<div v-show="photos.length > 0">
<div class="flex flex-wrap">
<div v-for="photo in photos" :key="photo.id" class="w-third-ns w-100">
<div v-if="photo.id > 0" class="pa2 mb3 br2 ba b--gray-monica" :class="dirltr ? 'mr3' : 'ml3'">
<div class="cover bg-center photo w-100 h-100 br2 bb b--gray-monica pb2"
:style="'background-image: url(' + photo.link + ');'"
>
</div>
<div class="pt2">
<ul>
<li>
<a class="pointer" href="" @click.prevent="deletePhoto(photo)">
{{ $t('people.photo_delete') }}
</a>
</li>
</ul>
</div>
</div>
<div v-else class="ba br3 photo-upload-zone mb3 pa3">
<div class="tc dib w-100 relative">
{{ $tc('app.file_selected', photos.length, {count: photos.length}) }}
</div>
</div>
</div>
</div>
</div>
</div>
<error :errors="errors" />
<!-- ACTIONS -->
<div class="pt3">
<div class="flex-ns justify-between">
<div class="">
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" @click.prevent="close">
{{ $t('app.cancel') }}
</a>
</div>
<div class="">
<button class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click.prevent="store">
{{ gift ? $t('app.update') : $t('app.add') }}
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import Error from '../../partials/Error.vue';
import PhotoUpload from '../photo/PhotoUpload.vue';
import { validationMixin } from 'vuelidate';
import { required, maxLength } from 'vuelidate/lib/validators';
export default {
components: {
Error,
PhotoUpload
},
mixins: [validationMixin],
model: {
prop: 'gift',
event: 'update'
},
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
},
gift: {
type: Object,
default: null,
},
familyContacts: {
type: Array,
default: () => [],
},
reachLimit: {
type: Boolean,
default: true,
},
},
data() {
return {
photos: [],
displayComment: false,
displayUrl: false,
displayAmount: false,
displayRecipient: false,
displayUpload: false,
displayDate: false,
newGift: {
name: '',
status: 'idea',
comment: null,
url: null,
amount: null,
date: null,
recipient_id: null,
photo_id: null,
},
hasRecipient: false,
errors: [],
};
},
validations() {
var v = {
newGift: {
name: {
required,
maxLength: maxLength(255),
},
}
};
if (this.hasRecipient) {
v.newGift = Object.assign(v.newGift, {
recipient_id: {
required,
}
});
}
return v;
},
computed: {
locale() {
return this.$root.locale;
},
dirltr() {
return this.$root.htmldir === 'ltr';
},
displayMenu() {
return !this.displayComment ||
!this.displayUrl ||
!this.displayAmount ||
!this.displayDate ||
!(this.displayRecipient || this.familyContacts.length === 0) ||
!(this.displayUpload || this.reachLimit);
}
},
watch: {
gift: function (val) {
this.newGift = val;
}
},
mounted() {
this.resetFields();
},
methods: {
resetFields() {
this.newGift.contact_id = this.contactId;
if (this.gift) {
this.newGift.contact_id = this.gift.contact.id;
this.newGift.name = this.gift.name;
this.newGift.comment = this.gift.comment;
this.newGift.url = this.gift.url;
this.newGift.amount = this.gift.amount;
this.newGift.status = this.gift.status;
this.newGift.recipient_id = this.gift.recipient ? this.gift.recipient.id : null;
this.hasRecipient = this.newGift.recipient_id !== null;
this.newGift.date = this.gift.date;
this.photos = this.gift.photos;
} else {
this.newGift.name = '';
this.newGift.comment = null;
this.newGift.url = null;
this.newGift.amount = null;
this.newGift.status = 'idea';
this.newGift.recipient_id = null;
this.newGift.date = null;
this.hasRecipient = false;
}
this.displayComment = this.gift ? this.gift.comment : false;
this.displayDate = this.gift ? this.gift.date : false;
this.displayUrl = this.gift ? this.gift.url : false;
this.displayAmount = this.gift ? this.gift.amount !== '' : false;
this.displayRecipient = this.gift ? (this.gift.recipient ? this.gift.recipient.id !== 0 : false) : false;
this.displayUpload= this.gift ? this.gift.photos.length > 0 : false;
this.errors = [];
this.$v.$reset();
},
close() {
this.resetFields();
this.$emit('cancel');
},
store() {
if (! this.hasRecipient) {
this.newGift.recipient_id = null;
}
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
const method = this.gift ? 'put' : 'post';
const url = `people/${this.hash}/gifts${this.gift ? '/'+this.gift.id : ''}`;
const vm = this;
axios[method](url, this.newGift)
.then(response => {
return vm.storePhoto(response);
})
.then(response => {
//this.update(response);
vm.close();
vm.$emit('update', response.data.data);
return response;
})
.then(() => {
this.$notify({
group: 'main',
title: vm.$t('people.gifts_add_success'),
text: '',
type: 'success'
});
})
.catch(error => {
vm._errorHandle(error);
});
},
storePhoto(response) {
const vm = this;
return this.$refs.upload.forceFileUpload()
.then(photo => {
if (photo !== undefined) {
axios.put(`people/${this.hash}/gifts/${response.data.data.id}/photo/${photo.id}`);
response.data.data.photos.push(photo);
}
return response;
})
.catch(error => {
vm._errorHandle(error);
return response;
});
},
_errorHandle(error) {
if (error.response && typeof error.response.data === 'object') {
this.errors = _.flatten(_.toArray(error.response.data));
} else {
this.errors = [vm.$t('app.error_try_again'), error.message];
}
},
deletePhoto(photo) {
axios.delete(`people/${this.hash}/photos/${photo.id}`)
.then(response => {
this.photos.splice(this.photos.indexOf(photo), 1);
if (this.photos.length === 0) {
this.$refs.upload.showUploadZone();
}
});
},
handlePhoto(event) {
this.photos.push({ id: -1, link: '' });
},
}
};
</script>

View File

@@ -0,0 +1,120 @@
<style scoped>
.photo {
height: 200px;
}
.gift-date{
float: right;
}
</style>
<template>
<div>
<p class="mb1 bb b--gray-monica pa2">
<span>
<a v-if="gift.url" :href="gift.url" rel="noopener noreferrer" target="_blank">
<strong>{{ gift.name }}</strong>
</a>
<strong v-else>{{ gift.name }}</strong>
</span>
<span v-if="gift.date" class="f7 gift-date">{{ formatDate(gift.date) }}</span>
<span v-if="gift.recipient && gift.recipient.complete_name">
<span class="black-50 mr1 ml1">
</span>
{{ $t('people.gifts_for', { name: gift.recipient.complete_name }) }}
</span>
</p>
<!-- LIST OF PHOTO -->
<div v-if="gift.photos !== undefined && gift.photos.length > 0" class="bb b--gray-monica pa1">
<div class="flex flex-wrap">
<div v-for="photo in gift.photos" :key="photo.id" class="w-third-ns w-100">
<div class="pa2 mb1 br2 ba b--gray-monica" :class="dirltr ? 'mr3' : 'ml3'">
<div class="cover bg-center photo w-100 h-auto br2 bb b--gray-monica pb2"
:style="'background-image: url(' + photo.link + ');'"
@click.prevent="modalPhoto(photo)"
>
</div>
</div>
</div>
</div>
</div>
<div v-if="gift.amount || gift.comment" class="f6 pv1 mb1 ph2 pb2 bb b--gray-monica">
<span v-if="gift.amount">
{{ gift.amount_with_currency }}
</span>
<span v-if="gift.amount && gift.comment" class="black-50 mr1 ml1">
</span>
<a v-if="gift.comment" class="pointer" href="" @click.prevent="comment = !comment">
{{ $t('people.gifts_view_comment') }}
</a>
<div v-if="comment" class="mb1 mt1">
{{ gift.comment }}
</div>
</div>
<div class="ph2 pb2 cf f7">
<slot></slot>
</div>
<!-- MODAL ZOOM PHOTO -->
<sweet-modal ref="modalPhoto" tabindex="-1" role="dialog" :enable-mobile-fullscreen="true" width="33%">
<img :src="url" :alt="$t('people.photo_title')" class="mw-90 h-auto mb3" />
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal,
},
props: {
gift: {
type: Object,
default: null,
},
},
data() {
return {
comment: false,
showModal: false,
url: '',
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
},
methods: {
modalPhoto(photo) {
this.url = photo.link;
this.$refs.modalPhoto.open();
},
formatDate(dateAsString) {
const moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var date = moment.tz(moment(dateAsString), this.$root.timezone);
return date.format('LL');
},
}
};
</script>

View File

@@ -0,0 +1,257 @@
<template>
<div>
<notifications group="main" position="bottom right" />
<!-- Title -->
<div>
<img src="img/people/gifts.svg" :alt="$t('people.gifts_title')" class="icon-section icon-tasks" />
<h3>
{{ $t('people.gifts_title') }}
<a v-cy-name="'add-gift-button'" href="" class="btn f6 pt2" :class="[ dirltr ? 'fr' : 'fl' ]"
@click.prevent="displayCreateGift = true"
>
{{ $t('people.gifts_add_gift') }}
</a>
</h3>
</div>
<template v-if="displayCreateGift">
<create-gift
:hash="hash"
:contact-id="contactId"
:family-contacts="familyContacts"
:reach-limit="reachLimit"
@update="updateList($event)"
@cancel="displayCreateGift = false"
/>
</template>
<!-- Listing -->
<div>
<ul class="mb3">
<li class="di">
<p class="di pointer" :class="[activeTab === 'idea' ? 'b' : 'black-50', dirltr ? 'mr3' : 'ml3']"
@click.prevent="setActiveTab('idea')"
>
{{ $t('people.gifts_ideas') }} ({{ ideas.length }})
</p>
</li>
<li class="di">
<p class="di pointer" :class="[activeTab === 'offered' ? 'b' : 'black-50', dirltr ? 'mr3' : 'ml3']"
@click.prevent="setActiveTab('offered')"
>
{{ $t('people.gifts_offered') }} ({{ offered.length }})
</p>
</li>
<li class="di">
<p class="di pointer" :class="[activeTab === 'received' ? 'b' : 'black-50', dirltr ? 'mr3' : 'ml3']"
@click.prevent="setActiveTab('received')"
>
{{ $t('people.gifts_received') }} ({{ received.length }})
</p>
</li>
</ul>
<div v-for="gift in filteredGifts" :key="gift.id" v-cy-name="'gift-item-' + gift.id" class="ba b--gray-monica mb3 br2">
<gift v-if="!gift.edit"
:gift="gift"
@update="($event) => { updateList($event) }"
>
<div :class="dirltr ? 'fl' : 'fr'">
<a v-if="gift.status === 'idea'" class="di" href="" @click.prevent="toggle(gift)">
{{ $t('people.gifts_mark_offered') }}
</a>
<a v-if="gift.status === 'offered'" class="di" href="" @click.prevent="toggle(gift)">
{{ $t('people.gifts_offered_as_an_idea') }}
</a>
</div>
<div :class="dirltr ? 'fr' : 'fl'">
<a v-cy-name="'edit-gift-button-' + gift.id" :class="dirltr ? 'mr1' : 'ml1'" class="di" href=""
@click.prevent="$set(gift, 'edit', true)"
>
{{ $t('app.edit') }}
</a>
<a v-cy-name="'delete-gift-button-' + gift.id" :class="dirltr ? 'mr1' : 'ml1'" class="di" href=""
@click.prevent="showDeleteModal(gift)"
>
{{ $t('app.delete') }}
</a>
</div>
</gift>
<create-gift
v-else
:hash="hash"
:gift="gift"
:contact-id="contactId"
:family-contacts="familyContacts"
:reach-limit="reachLimit"
@update="updateGift(gift, $event)"
@cancel="$set(gift, 'edit', false)"
/>
</div>
</div>
<sweet-modal ref="modal" overlay-theme="dark" :title="$t('people.gifts_delete_title')">
<form>
<div class="mb4">
{{ $t('people.gifts_delete_confirmation') }}
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'modal-delete-gift-button-' + giftToTrash.id" class="btn btn-primary" href="" @click.prevent="trash(giftToTrash)">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
import Gift from './Gift.vue';
import CreateGift from './CreateGift.vue';
import moment from 'moment';
export default {
components: {
Gift,
CreateGift,
SweetModal,
},
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
},
giftsActiveTab: {
type: String,
default: 'idea',
},
familyContacts: {
type: Array,
default: () => [],
},
reachLimit: {
type: Boolean,
default: true,
},
},
data() {
return {
gifts: [],
activeTab: '',
giftToTrash: '',
displayCreateGift: false,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
ideas() {
return this.gifts.filter(gift => gift.status === 'idea');
},
offered() {
return this.gifts.filter(gift => gift.status === 'offered');
},
received() {
return this.gifts.filter(gift => gift.status === 'received');
},
filteredGifts() {
const vm = this;
return this.gifts.filter(gift => gift.status === vm.activeTab);
},
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getGifts();
this.setActiveTab(this.giftsActiveTab);
},
setActiveTab(view) {
this.activeTab = view === 'ideas' ? 'idea' : view;
},
getGifts() {
axios.get(`people/${this.hash}/gifts`)
.then(response => {
this.gifts = response.data.data;
});
},
toggle(gift) {
if (gift.status === 'idea') {
gift.status = 'offered';
gift.date = moment().format('YYYY-MM-DD');
} else {
gift.status = 'idea';
gift.date = null;
}
gift.contact_id = this.contactId;
axios.put(`people/${this.hash}/gifts/${gift.id}`, gift)
.then(response => {
Vue.set(gift, 'status', response.data.data.status);
Vue.set(gift, 'date', response.data.data.date);
});
},
showDeleteModal(gift) {
this.$refs.modal.open();
this.giftToTrash = gift;
},
trash(gift) {
axios.delete(`people/${this.hash}/gifts/${gift.id}`)
.then(response => {
this.gifts.splice(this.gifts.indexOf(gift), 1);
this.closeDeleteModal();
});
},
updateList(activity) {
this.displayCreateGift = false;
this.getGifts();
},
updateGift(gift, response) {
this.$set(gift, 'edit', false);
this.$set(gift, 'name', response.name);
this.$set(gift, 'comment', response.comment);
this.$set(gift, 'url', response.url);
this.$set(gift, 'amount', response.amount);
this.$set(gift, 'amount_with_currency', response.amount_with_currency);
this.$set(gift, 'status', response.status);
this.$set(gift, 'recipient', response.recipient);
this.$set(gift, 'date', response.date);
this.$set(gift, 'photos', response.photos);
this.$emit('update', response);
},
closeDeleteModal() {
this.$refs.modal.close();
}
}
};
</script>

View File

@@ -0,0 +1,294 @@
<template>
<section class="ph3 ph0-ns life-event">
<notifications group="main" position="top middle" width="400" />
<div class="mt4 mw7 center mb3">
<!-- Breadcrumb -->
<ul v-if="view === 'types' || view === 'add'" class="ba b--gray-monica pa2 mb2">
<li class="di">
<a class="pointer" href="" @click.prevent="view = 'categories'">
{{ $t('people.life_event_create_category') }}
</a>
</li>
<li v-if="view === 'types'" class="di">
> {{ $t('people.life_event_category_' + activeCategory.default_life_event_category_key) }}
</li>
<template v-else-if="view === 'add'">
<li class="di">
&gt; <a class="pointer" href="" @click.prevent="view = 'types'">
{{ $t('people.life_event_category_' + activeCategory.default_life_event_category_key) }}
</a>
</li>
<li class="di">
&gt; {{ $t('people.life_event_create_life_event') }}
</li>
</template>
</ul>
<!-- List of events -->
<ul v-if="view !== 'add'" class="ba b--gray-monica br2">
<template v-if="view === 'categories'">
<!-- CATEGORIES -->
<li v-for="category in categories" :key="category.id" class="relative pointer bb b--gray-monica b--gray-monica pa2 life-event-add-row" @click="getType(category)">
<div class="dib mr2">
<img :src="'img/people/life-events/categories/' + category.default_life_event_category_key + '.svg'" :alt="category.default_life_event_category_key" style="min-width: 12px;" />
</div>
{{ $t('people.life_event_category_' + category.default_life_event_category_key) }}
<svg class="absolute life-event-add-arrow" width="10" height="13" viewBox="0 0 10 13" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.75071 5.66783C9.34483 6.06361 9.34483 6.93653 8.75072 7.33231L1.80442 11.9598C1.13984 12.4025 0.25 11.9261 0.25 11.1275L0.25 1.87263C0.25 1.07409 1.13984 0.59767 1.80442 1.04039L8.75071 5.66783Z" fill="#C4C4C4" />
</svg>
</li>
</template>
<template v-else-if="view === 'types'">
<!-- TYPES -->
<li v-for="type in types" :key="type.id" class="relative pointer bb b--gray-monica b--gray-monica pa2 life-event-add-row" @click="displayAddScreen(type)">
<div class="dib mr2">
<img :src="'img/people/life-events/types/' + type.default_life_event_type_key + '.svg'" :alt="type.default_life_event_type_key" style="min-width: 12px;" />
</div>
<template v-if="type.name">
{{ type.name }}
</template>
<template v-else>
{{ $t('people.life_event_sentence_' + type.default_life_event_type_key) }}
</template>
<svg class="absolute life-event-add-arrow" width="10" height="13" viewBox="0 0 10 13" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.75071 5.66783C9.34483 6.06361 9.34483 6.93653 8.75072 7.33231L1.80442 11.9598C1.13984 12.4025 0.25 11.9261 0.25 11.1275L0.25 1.87263C0.25 1.07409 1.13984 0.59767 1.80442 1.04039L8.75071 5.66783Z" fill="#C4C4C4" />
</svg>
</li>
</template>
</ul>
<!-- ADD SCREEN -->
<div v-else class="ba b--gray-monica br2 pt4">
<div class="life-event-add-icon tc center">
<img :src="'img/people/life-events/types/' + activeType.default_life_event_type_key + '.svg'" :alt="activeType.default_life_event_type_key" style="min-width: 17px;" />
</div>
<h3 class="pt3 ph4 f3 fw5 tc">
<template v-if="activeType.name">
{{ activeType.name }}
</template>
<template v-else>
{{ $t('people.life_event_sentence_' + activeType.default_life_event_type_key) }}
</template>
</h3>
<!-- This field will be the same for every life event type no matter what, as the date is the only required field -->
<div class="ph4 pv3 mb3 mb0-ns bb b--gray-monica">
<label for="year" class="mr2">
{{ $t('people.life_event_date_it_happened') }}
</label>
<div class="flex mb3">
<div class="mr2">
<form-select
:id="'year'"
v-model="selectedYear"
:options="years"
:title="''"
:class="[ dirltr ? 'mr2' : '' ]" @input="updateDate"
/>
</div>
<div class="mr2">
<form-select
:id="'month'"
v-model="selectedMonth"
:options="months"
:title="''"
:class="[ dirltr ? 'mr2' : '' ]" @input="updateDate"
/>
</div>
<div>
<form-select
:id="'day'"
v-model="selectedDay"
:options="days"
:title="''"
:class="[ dirltr ? '' : 'mr2' ]" @input="updateDate"
/>
</div>
</div>
<p class="f6">
{{ $t('people.life_event_create_date') }}
</p>
</div>
<create-default-life-event @contentChange="updateLifeEventContent($event)" />
<!-- YEARLY REMINDER -->
<div class="ph4 pv3 mb3 mb0-ns bb b--gray-monica">
<form-checkbox
v-model.lazy="newLifeEvent.has_reminder"
:name="'addReminder'"
:dclass="[ dirltr ? 'mr3' : 'ml3' ]"
>
{{ $t('people.life_event_create_add_yearly_reminder') }}
</form-checkbox>
</div>
<!-- FORM ACTIONS -->
<div class="ph4-ns ph3 pv3 bb b--gray-monica">
<div class="flex-ns justify-between">
<div>
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" href="" @click.prevent="$emit('dismissModal')">
{{ $t('app.cancel') }}
</a>
</div>
<div>
<button class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click="store()">
{{ $t('app.add') }}
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import moment from 'moment';
export default {
props: {
hash: {
type: String,
default: '',
},
years: {
type: Array,
default: function () {
return [];
}
},
months: {
type: Array,
default: function () {
return [];
}
},
days: {
type: Array,
default: function () {
return [];
}
},
},
data() {
return {
selectedDay: 0,
selectedMonth: 0,
selectedYear: 0,
newLifeEvent: {
name: '',
note: '',
happened_at: '',
life_event_type_id: 0,
happened_at_month_unknown: false,
happened_at_day_unknown: false,
specific_information: '',
has_reminder: false,
},
categories: [],
activeCategory: '',
activeType: '',
types: [],
view: 'categories',
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getCategories();
this.newLifeEvent.happened_at = moment().format('YYYY-MM-DD');
this.selectedYear = moment().year();
this.selectedMonth = moment().month() + 1; // month is zero indexed (O_o) in moments.js
this.selectedDay = moment().date();
},
displayAddScreen(type) {
this.view = 'add';
this.activeType = type;
this.newLifeEvent.life_event_type_id = type.id;
},
getCategories() {
axios.get('lifeevents/categories')
.then(response => {
this.categories = response.data.data;
});
},
getType(category) {
axios.get('lifeevents/categories/' + category.id + '/types')
.then(response => {
this.types = response.data.data;
});
this.view = 'types';
this.activeCategory = category;
},
updateLifeEventContent(lifeEvent) {
this.newLifeEvent.note = lifeEvent.note;
this.newLifeEvent.name = lifeEvent.name;
this.newLifeEvent.specific_information = lifeEvent.specific_information;
},
/**
* Sets the date when the user chooses either an empty month
* or an empty day of the month.
* If the user chooses an empty day, the day is set to 1 and we use
* a boolean to indicate that the day is unknown.
* Same for the month.
*/
updateDate() {
if (this.selectedDay === 0 || this.selectedDay === '0') {
this.newLifeEvent.happened_at_day_unknown = true;
this.newLifeEvent.happened_at = this.selectedYear + '-' + this.selectedMonth + '-01';
} else if (this.selectedMonth === 0 || this.selectedMonth === '0') {
this.newLifeEvent.happened_at_month_unknown = true;
this.newLifeEvent.happened_at_day_unknown = true;
this.newLifeEvent.happened_at = this.selectedYear + '-01-01';
this.selectedDay = 0;
} else {
this.newLifeEvent.happened_at = this.selectedYear + '-' + this.selectedMonth + '-' + this.selectedDay;
this.newLifeEvent.happened_at_month_unknown = false;
this.newLifeEvent.happened_at_day_unknown = false;
}
},
store() {
axios.post('people/' + this.hash + '/lifeevents', this.newLifeEvent)
.then(response => {
this.$emit('updateLifeEventTimeline', response.data);
this.$notify({
group: 'main',
title: this.$t('people.life_event_create_success'),
text: '',
type: 'success'
});
});
},
}
};
</script>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
<template>
<div>
<div class="ph4 pv3 mb3 mb0-ns bb b--gray-monica">
<label for="event-name" class="mr2">
{{ $t('people.life_event_create_default_title') }}
</label>
<input id="event-name" v-model="defaultEvent.name" autofocus class="br2 f5 w-100 ba b--black-40 pa2 outline-0" @input="broadcastContentChange" />
</div>
<div class="ph4 pv3 mb3 mb0-ns bb b--gray-monica">
<label for="description" class="mr2">
{{ $t('people.life_event_create_default_story') }}
</label>
<form-textarea
id="description"
v-model="defaultEvent.note"
:required="false"
:no-label="true"
:rows="4"
:placeholder="$t('people.life_event_create_default_description')"
@input="broadcastContentChange"
/>
</div>
</div>
</template>
<script>
import moment from 'moment';
export default {
data() {
return {
defaultEvent: {
name: '',
note: '',
specific_information: '',
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.defaultEvent.happened_at = moment().format('YYYY-MM-DD');
},
broadcastContentChange() {
this.$emit('contentChange', this.defaultEvent);
},
}
};
</script>

View File

@@ -0,0 +1,226 @@
<style lang="scss">
.autosuggest__results-container {
position: relative;
width: 100%;
}
.autosuggest__results {
position: absolute;
width: 100%;
z-index: 100;
}
.autosuggest__results-overflow {
position: absolute;
width: 100%;
z-index: 100;
overflow: scroll;
max-height: 361px;
}
.autosuggest__results-item {
background: white;
}
.autosuggest__results-item:active,
.autosuggest__results-item:hover,
.autosuggest__results-item:focus,
.autosuggest__results-item--highlighted {
background: #f5f5f5;
}
</style>
<template>
<div>
<label
v-if="title"
class="mb2"
:class="{ b: required }"
:for="realid"
>
{{ title }}
</label>
<vue-autosuggest
ref="autosuggest"
:suggestions="items"
:input-props="inputProps"
:get-suggestion-value="getSuggestionValue"
:component-attr-class-autosuggest-results="overflow ? 'autosuggest__results-overflow' : 'autosuggest__results'"
@selected="selectHandler"
@click="clickHandler"
@blur="blurHandler"
@input="updateItems"
>
<template slot-scope="{suggestion}">
<component :is="componentItem" :item="suggestion.item" />
</template>
</vue-autosuggest>
</div>
</template>
<script>
import axios from 'axios';
import { VueAutosuggest } from 'vue-autosuggest';
export default {
components: {
VueAutosuggest,
},
props: {
id: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
required: {
type: Boolean,
default: true,
},
addNoResult: {
type: Boolean,
default: true,
},
placeholder : {
type: String,
default: '',
},
componentItem : {
type: Object,
default: () => null
},
wait : {
type: Number,
default: 200,
},
minLen : {
type: Number,
default: 1,
},
overflow: {
type: Boolean,
default: false,
},
inputClass : {
type: String,
default: '',
},
filter: {
type: Function,
default: () => true,
}
},
data () {
return {
items: [],
callUpdateItems: null,
cache: [],
};
},
computed: {
realid() {
return this.id ? this.id : 'autosuggest__input';
},
inputProps() {
return {
id: this.realid,
placeholder: this.placeholder,
class: ['form-control', this.inputClass],
};
}
},
mounted() {
this.callUpdateItems = _.debounce((text) => {
this.getContacts(text, this)
.then((response) => {
this.cache[text] = response;
this.displayItems(text);
});
}, this.wait);
},
methods: {
updateItems (text) {
if (text === null || text === undefined) {
return;
}
if (text.length < this.minLen) {
this.items = [];
return;
}
if (this.cache[text] === undefined) {
this.callUpdateItems(text);
} else {
this.callUpdateItems.cancel();
this.displayItems(text);
}
},
displayItems (text) {
var datas = this.cache[text];
datas = datas.filter(this.filter);
this.items = [{ data: datas }];
},
getContacts: function (keyword, vm) {
return axios.post('people/search', {
needle: keyword
}).then(function(response) {
const data = [];
if (response.data.noResults === undefined || response.data.noResults === null) {
response.data.data
.forEach(function (contact) {
contact.keyword = keyword;
data.push(contact);
});
}
data.push({
id: -1,
name: 'add_new_contact',
keyword: keyword,
});
return data;
});
},
clearCache() {
this.cache = [];
this.items = [];
},
blurHandler(sender) {
this.$emit('blur', sender);
},
clickHandler(e) {
this.loading = false;
this.updateItems(this.value);
},
selectHandler(suggestion) {
if (!suggestion || !suggestion.item) {
return;
}
this.$emit('select', suggestion);
this.$refs.autosuggest.searchInput = '';
},
getSuggestionValue(suggestion) {
if (!suggestion || !suggestion.item || suggestion.item.id < 0) {
return;
}
return suggestion.item.complete_name.length > 0 ?
suggestion.item.complete_name :
suggestion.item.keyword;
}
}
};
</script>

View File

@@ -0,0 +1,106 @@
<style lang="scss" scoped>
.avatar-new {
background-color: #fdb660;
}
.item-search-result {
position: relative;
background: transparent;
a {
color: inherit;
text-decoration: none;
vertical-align: middle;
background: transparent;
span {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
}
}
a:hover {
background: inherit;
color: inherit;
}
.avatar {
border-radius: 3px;
display: inline-block;
height: 36px;
margin: 10px;
width: 36px;
}
.avatar-initials {
text-align: center;
padding-top: 6px;
font-size: 15px;
color: #fff;
}
&:last-child {
border-bottom: initial;
}
}
</style>
<template>
<div v-if="item.id > 0" class="item-search-result" :data-contact="item.id" :data-name="item.name">
<a :href="item.route">
<img v-if="check"
:class="className"
:src="item.information.avatar.url"
:alt="item.complete_name"
@error="check=false"
/>
<div v-else
:class="[className, 'avatar-initials']"
:style="'background-color: '+item.information.avatar.default_avatar_color"
>
{{ item.initials }}
</div>
<template v-if="withName">
{{ item.complete_name }}
</template>
<span></span>
</a>
</div>
<div v-else class="item-search-result">
<div class="avatar avatar-initials avatar-new">
+
</div>
{{ $t('people.people_add_new') }}
<span></span>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
default: null,
},
withName: {
type: Boolean,
default: true,
},
className: {
type: String,
default: 'avatar',
}
},
data() {
return {
check: true,
};
},
};
</script>

View File

@@ -0,0 +1,97 @@
<style lang="scss" scoped>
.avatar-new {
background-color: #fdb660;
}
.item-search-result {
position: relative;
background: transparent;
border: 1px solid #c4cdd5;
.item-search-result-result {
color: inherit;
vertical-align: middle;
background: transparent;
span {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
}
}
.avatar {
border-radius: 3px;
display: inline-block;
height: 36px;
margin: 10px;
width: 36px;
}
.avatar-initials {
text-align: center;
padding-top: 6px;
font-size: 15px;
color: #fff;
}
.avatar-no-results {
color: #fdb660;
}
&:first-child {
border-top: initial;
}
}
</style>
<template>
<div v-if="item.id > 0" class="item-search-result" :data-contact="item.id" :data-name="item.name">
<div class="item-search-result-result pointer">
<img v-if="check"
class="avatar"
:src="item.information.avatar.url"
:alt="item.complete_name"
@error="check=false"
/>
<div v-else
class="avatar avatar-initials"
:style="'background-color: '+item.information.avatar.default_avatar_color"
>
{{ item.initials }}
</div>
{{ item.complete_name }}
<span></span>
</div>
</div>
<div v-else class="item-search-result">
<div class="item-search-result-result pointer">
<div class="avatar avatar-initials avatar-no-results">
.
</div>
{{ $t('people.people_search_no_results') }}
<span></span>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
default: null,
},
},
data() {
return {
check: true,
};
},
};
</script>

View File

@@ -0,0 +1,279 @@
<style scoped>
.photo {
height: 250px;
}
</style>
<template>
<div>
<div class="">
<h3>
📄 {{ $t('people.photo_list_title') }}
<span v-if="reachLimit === 'false'" class="fr relative" style="top: -7px;">
<a v-if="!onUpload" class="btn" href=""
@click.prevent="() => { onUpload = true; $refs.upload.showUploadZone(); }"
>
{{ $t('people.photo_list_cta') }}
</a>
<a v-else class="btn" href=""
@click.prevent="() => { onUpload = false; $refs.upload.cancelUpload(); }"
>
{{ $t('app.cancel') }}
</a>
</span>
</h3>
</div>
<p v-show="reachLimit === 'true'">
{{ $t('settings.storage_upgrade_notice') }}
</p>
<!-- EMPTY STATE -->
<div v-if="!onUpload && photos.length === 0" class="ltr w-100 pt2">
<div class="section-blank">
<h3 class="mb4 mt3">
{{ $t('people.photo_list_blank_desc') }}
</h3>
<img src="img/people/photos/photos_empty.svg" :alt="$t('people.photo_title')" class="w-50 center" />
</div>
</div>
<photo-upload
ref="upload"
:hash="hash"
:contact-id="contactId"
@newphoto="handleNewPhoto($event)"
/>
<!-- LIST OF PHOTO -->
<div class="db mt3">
<div class="flex flex-wrap">
<div v-for="photo in photos" :key="photo.id" class="w-third-ns w-100 pointer">
<div class="pa3 mb3 br2 ba b--gray-monica" :class="dirltr ? 'mr3' : 'ml3'">
<div class="cover bg-center photo w-100 h-100 br2 bb b--gray-monica pb2"
:style="'background-image: url(' + photo.link + ');'"
@click.prevent="modalPhoto(photo)"
>
</div>
<div class="pt2">
<ul>
<li v-show="currentPhotoIdAsAvatar === photo.id">
🤩 {{ $t('people.photo_current_profile_pic') }}
</li>
<li v-show="currentPhotoIdAsAvatar !== photo.id">
<a class="pointer" @click.prevent="makeProfilePicture(photo)">
{{ $t('people.photo_make_profile_pic') }}
</a>
</li>
<li v-show="confirmDestroyPhotoId !== photo.id">
<a class="pointer" href="" @click.prevent="confirmDestroyPhotoId = photo.id">
{{ $t('people.photo_delete') }}
</a>
</li>
<li v-show="confirmDestroyPhotoId === photo.id">
<a class="pointer" href="" @click.prevent="confirmDestroyPhotoId = 0">
{{ $t('app.cancel') }}
</a> <a class="pointer" href="" @click.prevent="deletePhoto(photo)">
{{ $t('app.delete_confirm') }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL ZOOM PHOTO -->
<transition v-if="showModal" name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<img :src="url" :alt="$t('people.photo_title')" class="mw-90 h-auto mb3" />
<ul class="list pl0 tc">
<li class="di mr3">
<a v-if="canShowPrev" class="pointer" @click="displayPrev">{{ $t('people.photo_previous') }}</a>
</li>
<li class="di mr3">
<button class="btn" @click="showModal = false">
{{ $t('app.close') }}
</button>
</li>
<li class="di">
<a v-if="canShowNext" class="pointer" @click="displayNext">{{ $t('people.photo_next') }}</a>
</li>
</ul>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import PhotoUpload from './PhotoUpload.vue';
export default {
components: {
PhotoUpload,
},
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
},
reachLimit: {
type: String,
default: '',
},
currentPhotoIdAsAvatar: {
type: String,
default: '',
},
},
data() {
return {
photos: [],
file: '',
uploadPercentage: 0,
confirmDestroyPhotoId: 0,
showModal: false,
url: '',
onUpload: false,
canShowPrev: false,
canShowNext: false
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getPhotos();
},
getPhotos() {
axios.get('people/' + this.hash + '/photos')
.then(response => {
this.photos = response.data.data;
});
},
handleNewPhoto(photo) {
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
this.photos.push(photo);
},
deletePhoto(photo) {
axios.delete( 'people/' + this.hash + '/photos/' + photo.id)
.then(response => {
this.photos.splice(this.photos.indexOf(photo), 1);
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
makeProfilePicture(photo) {
axios.post( 'people/' + this.hash + '/makeProfilePicture/' + photo.id)
.then(response => {
window.location.href = 'people/' + this.hash;
});
},
modalPhoto(photo) {
this.modal_photo = photo;
this.url = photo.link;
if(this.modalHasNext()) {
this.canShowNext = true;
} else {
this.canShowNext = false;
}
if(this.modalHasPrev()) {
this.canShowPrev = true;
} else {
this.canShowPrev = false;
}
this.showModal = true;
},
/**
* checks whether the there is a photo to the left of the current
* photo shown in the modal
*
* @return {Boolean}
*/
modalHasNext() {
const index = this.photos.indexOf(this.modal_photo);
return index < this.photos.length-1;
},
/**
* checks whether the there is a photo to the right of the current
* photo shown in the modal
*
* @return {Boolean}
*/
modalHasPrev() {
const index = this.photos.indexOf(this.modal_photo);
return index > 0;
},
/**
* set photo in modal located to the right of current modal photo
* from our photos list
*/
displayNext() {
const index = this.photos.indexOf(this.modal_photo);
const photo = this.photos[index+1];
this.modalPhoto(photo);
},
/**
* set photo in modal located to the left of current modal photo
* from our photos list
*/
displayPrev() {
const index = this.photos.indexOf(this.modal_photo);
const photo = this.photos[index-1];
this.modalPhoto(photo);
}
}
};
</script>

View File

@@ -0,0 +1,179 @@
<style scoped lang="scss">
.photo-upload-zone {
background: #fff;
border: 1px solid #d6d6d6;
border-style: dashed;
}
progress {
-webkit-appearance: none;
border: none;
height: 8px;
margin-bottom: 3px;
width: 60%;
}
progress::-webkit-progress-bar {
background: #e2e7ee;
border-radius: 3px;
}
progress::-webkit-progress-value {
border-radius: 3px;
box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
background-color: #329FF1;
}
</style>
<template>
<div>
<!-- FIRST STEP OF PHOTO UPLOAD -->
<transition name="fade">
<div v-if="displayUploadZone" class="ba br3 photo-upload-zone mb3 pa3">
<div class="tc">
</div>
<div class="tc dib w-100 relative">
<button class="btn">
{{ $t('people.photo_upload_zone_cta') }}
</button>
<input id="file" ref="file" type="file" class="absolute o-0 w-100 h-100 pointer" style="left:0;"
@change="handleFileUpload($event)"
/>
</div>
</div>
</transition>
<!-- LAST STEP OF PHOTO UPLOAD -->
<div v-if="displayUploadProgress" class="ba br3 photo-upload-zone mb3 pa3">
<p class="tc mb1">
{{ $t('people.document_upload_zone_progress') }}
</p>
<div class="tc mb1">
<progress max="100" :value.prop="uploadPercentage"></progress>
</div>
<p class="tc f6 mb0">
{{ $t('app.percent_uploaded', {percent: uploadPercentage}) }}
</p>
</div>
<!-- ERROR STEP WHEN UPLOADING A PHOTO -->
<transition name="fade">
<div v-if="displayUploadError" class="ba br3 photo-upload-zone mb3 pa3">
<div class="tc">
<svg width="120" height="150" viewBox="0 0 120 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.2012 35.9138V101.562H92.7999V18.4375H44.6762L27.2012 35.9138ZM44.1274 21.6388V35.365H30.4024L44.1274 21.6388ZM46.0024 20.3125H90.9249V99.6875H29.0762V37.24H46.0024V20.3125Z" fill="#868E99" />
<path d="M83.6988 72.3676H36.3013V74.2426H83.6988V72.3676Z" fill="#868E99" />
<path d="M83.6988 82.245H36.3013V84.12H83.6988V82.245Z" fill="#868E99" />
<path d="M83.6988 92.1226H36.3013V93.9976H83.6988V92.1226Z" fill="#868E99" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M78.5862 46.875C79.367 46.875 80 46.2314 80 45.4375C80 44.6436 79.367 44 78.5862 44H68.6897C67.9088 44 67.2759 44.6436 67.2759 45.4375C67.2759 47.8192 69.1748 49.75 71.5172 49.75C73.364 49.75 74.935 48.5499 75.5173 46.875H78.5862ZM40.4138 46.875C39.633 46.875 39 46.2314 39 45.4375C39 44.6436 39.633 44 40.4138 44H50.3103C51.0912 44 51.7241 44.6436 51.7241 45.4375C51.7241 47.8192 49.8252 49.75 47.4828 49.75C45.636 49.75 44.065 48.5499 43.4827 46.875H40.4138ZM55.7524 66.3213C55.888 66.0995 56.2309 65.69 56.7753 65.2677C57.698 64.552 58.8279 64.1248 60.2069 64.1248C61.5859 64.1248 62.7158 64.552 63.6385 65.2677C64.1829 65.69 64.5258 66.0995 64.6614 66.3213C65.0736 66.9955 65.9454 67.2023 66.6085 66.7831C67.2716 66.364 67.475 65.4776 67.0628 64.8034C66.7589 64.3064 66.199 63.6378 65.3535 62.9819C63.9596 61.9008 62.2381 61.2499 60.2069 61.2499C58.1757 61.2499 56.4542 61.9008 55.0603 62.9819C54.2148 63.6378 53.6549 64.3064 53.351 64.8034C52.9388 65.4776 53.1422 66.364 53.8053 66.7831C54.4684 67.2023 55.3402 66.9955 55.7524 66.3213Z" fill="#868E99" />
</svg>
</div>
<p class="tc mb3">
{{ $t('people.document_upload_zone_error') }}
</p>
<p class="tc">
<input id="file" ref="file" type="file" @change="handleFileUpload($event)" />
</p>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
hash: {
type: String,
default: '',
},
contactId: {
type: Number,
default: 0,
},
reachLimit: {
type: String,
default: '',
},
currentPhotoIdAsAvatar: {
type: String,
default: '',
},
},
data() {
return {
displayUploadZone: false,
displayUploadProgress: false,
displayUploadError: false,
file: '',
uploadPercentage: 0,
confirmDestroyPhotoId: 0,
url: '',
};
},
methods: {
showUploadZone() {
this.displayUploadZone = true;
},
inUpload() {
return this.displayUploadZone || this.displayUploadError || this.displayUploadProgress;
},
cancelUpload() {
this.displayUploadZone = false;
this.displayUploadError = false;
this.displayUploadProgress = false;
},
handleFileUpload(event){
this.$emit('upload', event);
if (!event.cancelBubble) {
this.forceFileUpload();
}
},
forceFileUpload(){
const f = this.$refs.file !== undefined ? this.$refs.file.files[0] : undefined;
if (f === undefined) {
return Promise.resolve();
}
this.file = f;
return this.submitFile();
},
submitFile(){
this.displayUploadZone = false;
this.displayUploadProgress = true;
const formData = new FormData();
formData.append('photo', this.file);
return axios.post(`people/${this.hash}/photos`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: function( progressEvent ) {
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
}.bind(this)
}
).then(response => {
this.displayUploadProgress = false;
this.$emit('newphoto', response.data.data);
return response.data.data;
})
.catch(error => {
this.displayUploadProgress = false;
this.file = null;
this.displayUploadError = true;
});
},
}
};
</script>