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

343
resources/js/app.js vendored Normal file
View File

@@ -0,0 +1,343 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
import Vue from 'vue';
window.Vue = Vue;
// Notifications
import Notifications from 'vue-notification';
Vue.use(Notifications);
// Tooltip
import Tooltip from 'vue-directive-tooltip';
Vue.use(Tooltip, { delay: 0 });
// Copy text from clipboard
import VueClipboard from 'vue-clipboard2';
VueClipboard.config.autoSetContainer = true;
Vue.use(VueClipboard);
// Dependency of vuejs-clipper
import VueRx from 'vue-rx';
Vue.use(VueRx);
// Custom components
Vue.component(
'PassportClients',
require('./components/passport/Clients.vue').default
);
Vue.component(
'PassportAuthorizedClients',
require('./components/passport/AuthorizedClients.vue').default
);
Vue.component(
'PassportPersonalAccessTokens',
require('./components/passport/PersonalAccessTokens.vue').default
);
// Vue select
Vue.component(
'ContactSelect',
require('./components/people/ContactSelect.vue').default
);
Vue.component(
'ContactSearch',
require('./components/people/ContactSearch.vue').default
);
Vue.component(
'ContactMultiSearch',
require('./components/people/ContactMultiSearch.vue').default
);
// Partials
Vue.component(
'Avatar',
require('./components/partials/Avatar.vue').default
);
Vue.component(
'Confirm',
require('./components/partials/Confirm.vue').default
);
// Form elements
Vue.component(
'FormInput',
require('./components/partials/form/Input.vue').default
);
Vue.component(
'FormSelect',
require('./components/partials/form/Select.vue').default
);
Vue.component(
'FormDate',
require('./components/partials/form/Date.vue').default
);
Vue.component(
'FormCheckbox',
require('./components/partials/form/Checkbox.vue').default
);
Vue.component(
'FormRadio',
require('./components/partials/form/Radio.vue').default
);
Vue.component(
'FormTextarea',
require('./components/partials/form/Textarea.vue').default
);
Vue.component(
'FormToggle',
require('./components/partials/form/Toggle.vue').default
);
Vue.component(
'FormSpecialdate',
require('./components/partials/SpecialDate.vue').default
);
Vue.component(
'FormSpecialdeceased',
require('./components/partials/SpecialDeceased.vue').default
);
// Dashboard
Vue.component(
'DashboardLog',
require('./components/dashboard/DashboardLog.vue').default
);
// Contacts
Vue.component(
'Tags',
require('./components/people/Tags.vue').default
);
Vue.component(
'ContactAvatar',
require('./components/people/SetAvatar.vue').default
);
Vue.component(
'ContactFavorite',
require('./components/people/SetFavorite.vue').default
);
Vue.component(
'ContactArchive',
require('./components/people/Archive.vue').default
);
Vue.component(
'ContactAddress',
require('./components/people/Addresses.vue').default
);
Vue.component(
'ContactInformation',
require('./components/people/ContactInformation.vue').default
);
Vue.component(
'ContactList',
require('./components/people/ContactList.vue').default
);
Vue.component(
'ContactTask',
require('./components/people/Tasks.vue').default
);
Vue.component(
'ContactNote',
require('./components/people/Notes.vue').default
);
Vue.component(
'ContactGift',
require('./components/people/gifts/Gifts.vue').default
);
Vue.component(
'Pet',
require('./components/people/Pets.vue').default
);
Vue.component(
'MeContact',
require('./components/people/MeContact.vue').default
);
Vue.component(
'StayInTouch',
require('./components/people/StayInTouch.vue').default
);
Vue.component(
'LastCalled',
require('./components/people/calls/LastCalled.vue').default
);
Vue.component(
'PhoneCallList',
require('./components/people/calls/PhoneCallList.vue').default
);
Vue.component(
'ConversationList',
require('./components/people/conversation/ConversationList.vue').default
);
Vue.component(
'Conversation',
require('./components/people/conversation/Conversation.vue').default
);
Vue.component(
'Message',
require('./components/people/conversation/Message.vue').default
);
Vue.component(
'ActivityList',
require('./components/people/activity/ActivityList.vue').default
);
Vue.component(
'DocumentList',
require('./components/people/document/DocumentList.vue').default
);
Vue.component(
'CreateLifeEvent',
require('./components/people/lifeevent/CreateLifeEvent.vue').default
);
Vue.component(
'CreateDefaultLifeEvent',
require('./components/people/lifeevent/content/CreateDefaultLifeEvent.vue').default
);
Vue.component(
'LifeEventList',
require('./components/people/lifeevent/LifeEventList.vue').default
);
Vue.component(
'PhotoList',
require('./components/people/photo/PhotoList.vue').default
);
// Journal
Vue.component(
'JournalList',
require('./components/journal/JournalList.vue').default
);
Vue.component(
'JournalRateDay',
require('./components/journal/RateDay.vue').default
);
Vue.component(
'JournalCalendar',
require('./components/journal/partials/JournalCalendar.vue').default
);
Vue.component(
'JournalContentRate',
require('./components/journal/partials/JournalContentRate.vue').default
);
Vue.component(
'JournalContentActivity',
require('./components/journal/partials/JournalContentActivity.vue').default
);
Vue.component(
'JournalContentEntry',
require('./components/journal/partials/JournalContentEntry.vue').default
);
// Settings
Vue.component(
'ContactFieldTypes',
require('./components/settings/ContactFieldTypes.vue').default
);
Vue.component(
'Genders',
require('./components/settings/Genders.vue').default
);
Vue.component(
'ReminderRules',
require('./components/settings/ReminderRules.vue').default
);
Vue.component(
'ReminderTime',
require('./components/settings/ReminderTime.vue').default
);
Vue.component(
'MfaActivate',
require('./components/settings/MfaActivate.vue').default
);
Vue.component(
'WebauthnConnector',
require('./components/settings/WebauthnConnector.vue').default
);
Vue.component(
'RecoveryCodes',
require('./components/settings/RecoveryCodes.vue').default
);
Vue.component(
'Modules',
require('./components/settings/Modules.vue').default
);
Vue.component(
'ActivityTypes',
require('./components/settings/ActivityTypes.vue').default
);
Vue.component(
'LifeEventTypes',
require('./components/settings/LifeEventTypes.vue').default
);
Vue.component(
'DavResources',
require('./components/settings/DAVResources.vue').default
);
require('./testing');
var common = require('./common').default;
common.loadLanguage(window.Laravel.locale, true).then((i18n) => {
// the Vue appplication
const app = new Vue({
i18n,
data: {
htmldir: window.Laravel.htmldir,
timezone: window.Laravel.timezone,
locale: i18n.locale,
reminders_frequency: 'once',
accept_invite_user: false,
date_met_the_contact: 'known',
global_relationship_form_new_contact: true,
global_profile_default_view: window.Laravel.profileDefaultView,
},
// global methods
methods: require('./methods').default
}).$mount('#app');
return app;
});
$(document).ready(function() {
});

47
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,47 @@
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap/js/dist/util');
require('bootstrap/js/dist/button');
require('bootstrap/js/dist/collapse');
require('bootstrap/js/dist/dropdown');
require('bootstrap/js/dist/modal');
require('bootstrap/js/dist/tab');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });

63
resources/js/common.js vendored Normal file
View File

@@ -0,0 +1,63 @@
'use strict';
// axios
import axios from 'axios';
// i18n
import VueI18n from 'vue-i18n';
Vue.use(VueI18n);
// Moments
import moment from 'moment';
Vue.filter('formatDate', function(value) {
if (value) {
return moment(String(value)).format('LL');
}
});
// Markdown
window.marked = require('marked');
// i18n
import messages from '../../public/js/langs/en.json';
import pluralization from './pluralization.js';
export default {
i18n: new VueI18n({
locale: 'en', // set locale
fallbackLocale: 'en',
messages: {'en': messages},
pluralizationRules: pluralization,
}),
loadedLanguages : ['en'], // our default language that is preloaded
_setI18nLanguage (lang) {
this.i18n.locale = lang;
axios.defaults.headers.common['Accept-Language'] = lang;
document.querySelector('html').setAttribute('lang', lang);
},
_loadLanguageAsync (lang) {
if (this.i18n.locale !== lang) {
if (!this.loadedLanguages.includes(lang)) {
return axios.get(`js/langs/${lang}.json`).then(msgs => {
this.i18n.setLocaleMessage(lang, msgs.data);
this.loadedLanguages.push(lang);
return this.i18n;
});
}
}
return Promise.resolve(this.i18n);
},
loadLanguage: function(lang, set) {
return this._loadLanguageAsync(lang).then(i18n => {
if (set) {
this._setI18nLanguage(lang);
}
moment.locale(lang === 'zh' ? 'zh-cn' : lang);
return i18n;
});
}
};

View File

@@ -0,0 +1,457 @@
<template>
<div class="br3 ba b--gray-monica bg-white mb3">
<notifications group="main" position="bottom right" width="400" />
<div class="pa3 bb b--gray-monica tc">
<ul>
<li :class="[activeTab === 'calls' ? 'di pointer mr3 b' : 'di pointer mr3 black-50']" @click.prevent="setActiveTab('calls')">
{{ $t('dashboard.tab_recent_calls') }}
</li>
<li :class="[activeTab === 'notes' ? 'di pointer mr3 b' : 'di pointer mr3 black-50']" @click.prevent="setActiveTab('notes')">
{{ $t('dashboard.tab_favorite_notes') }}
</li>
<li :class="[activeTab === 'debts' ? 'di pointer mr3 b' : 'di pointer mr3 black-50']" @click.prevent="setActiveTab('debts')">
{{ $t('dashboard.tab_debts') }}
</li>
<li :class="[activeTab === 'tasks' ? 'di pointer mr3 b' : 'di pointer mr3 black-50']" @click.prevent="setActiveTab('tasks')">
{{ $t('dashboard.tab_tasks') }}
</li>
</ul>
</div>
<div class="pa4">
<!-- Calls -->
<div v-if="activeTab === 'calls'">
<ul v-if="calls.length !== 0">
<li v-for="call in calls" :key="call.id" class="pb2">
<svg id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px"
y="0px" viewBox="0 0 473.806 473.806" style="enable-background:new 0 0 473.806 473.806;" xml:space="preserve" width="15px"
height="15px" class="mr2"
>
<g>
<g>
<path d="M374.456,293.506c-9.7-10.1-21.4-15.5-33.8-15.5c-12.3,0-24.1,5.3-34.2,15.4l-31.6,31.5c-2.6-1.4-5.2-2.7-7.7-4 c-3.6-1.8-7-3.5-9.9-5.3c-29.6-18.8-56.5-43.3-82.3-75c-12.5-15.8-20.9-29.1-27-42.6c8.2-7.5,15.8-15.3,23.2-22.8 c2.8-2.8,5.6-5.7,8.4-8.5c21-21,21-48.2,0-69.2l-27.3-27.3c-3.1-3.1-6.3-6.3-9.3-9.5c-6-6.2-12.3-12.6-18.8-18.6 c-9.7-9.6-21.3-14.7-33.5-14.7s-24,5.1-34,14.7c-0.1,0.1-0.1,0.1-0.2,0.2l-34,34.3c-12.8,12.8-20.1,28.4-21.7,46.5 c-2.4,29.2,6.2,56.4,12.8,74.2c16.2,43.7,40.4,84.2,76.5,127.6c43.8,52.3,96.5,93.6,156.7,122.7c23,10.9,53.7,23.8,88,26 c2.1,0.1,4.3,0.2,6.3,0.2c23.1,0,42.5-8.3,57.7-24.8c0.1-0.2,0.3-0.3,0.4-0.5c5.2-6.3,11.2-12,17.5-18.1c4.3-4.1,8.7-8.4,13-12.9 c9.9-10.3,15.1-22.3,15.1-34.6c0-12.4-5.3-24.3-15.4-34.3L374.456,293.506z M410.256,398.806 C410.156,398.806,410.156,398.906,410.256,398.806c-3.9,4.2-7.9,8-12.2,12.2c-6.5,6.2-13.1,12.7-19.3,20 c-10.1,10.8-22,15.9-37.6,15.9c-1.5,0-3.1,0-4.6-0.1c-29.7-1.9-57.3-13.5-78-23.4c-56.6-27.4-106.3-66.3-147.6-115.6 c-34.1-41.1-56.9-79.1-72-119.9c-9.3-24.9-12.7-44.3-11.2-62.6c1-11.7,5.5-21.4,13.8-29.7l34.1-34.1c4.9-4.6,10.1-7.1,15.2-7.1 c6.3,0,11.4,3.8,14.6,7c0.1,0.1,0.2,0.2,0.3,0.3c6.1,5.7,11.9,11.6,18,17.9c3.1,3.2,6.3,6.4,9.5,9.7l27.3,27.3 c10.6,10.6,10.6,20.4,0,31c-2.9,2.9-5.7,5.8-8.6,8.6c-8.4,8.6-16.4,16.6-25.1,24.4c-0.2,0.2-0.4,0.3-0.5,0.5 c-8.6,8.6-7,17-5.2,22.7c0.1,0.3,0.2,0.6,0.3,0.9c7.1,17.2,17.1,33.4,32.3,52.7l0.1,0.1c27.6,34,56.7,60.5,88.8,80.8 c4.1,2.6,8.3,4.7,12.3,6.7c3.6,1.8,7,3.5,9.9,5.3c0.4,0.2,0.8,0.5,1.2,0.7c3.4,1.7,6.6,2.5,9.9,2.5c8.3,0,13.5-5.2,15.2-6.9 l34.2-34.2c3.4-3.4,8.8-7.5,15.1-7.5c6.2,0,11.3,3.9,14.4,7.3c0.1,0.1,0.1,0.1,0.2,0.2l55.1,55.1 C420.456,377.706,420.456,388.206,410.256,398.806z" fill="#8c8c8c" />
<path d="M256.056,112.706c26.2,4.4,50,16.8,69,35.8s31.3,42.8,35.8,69c1.1,6.6,6.8,11.2,13.3,11.2c0.8,0,1.5-0.1,2.3-0.2 c7.4-1.2,12.3-8.2,11.1-15.6c-5.4-31.7-20.4-60.6-43.3-83.5s-51.8-37.9-83.5-43.3c-7.4-1.2-14.3,3.7-15.6,11 S248.656,111.506,256.056,112.706z" fill="#8c8c8c" />
<path d="M473.256,209.006c-8.9-52.2-33.5-99.7-71.3-137.5s-85.3-62.4-137.5-71.3c-7.3-1.3-14.2,3.7-15.5,11 c-1.2,7.4,3.7,14.3,11.1,15.6c46.6,7.9,89.1,30,122.9,63.7c33.8,33.8,55.8,76.3,63.7,122.9c1.1,6.6,6.8,11.2,13.3,11.2 c0.8,0,1.5-0.1,2.3-0.2C469.556,223.306,474.556,216.306,473.256,209.006z" fill="#8c8c8c" />
</g>
</g>
</svg>
<span class="black-50 mr1 f6">
{{ call.called_at }}
</span>
<span class="mr1 black-50">
•
</span>
<a :href="'people/' + call.contact_id">
{{ call.name }}
</a>
</li>
</ul>
<!-- Calls: Blank state -->
<div v-if="calls.length === 0" class="tc mt4 mb4">
<p>{{ $t('dashboard.tab_calls_blank') }}</p>
</div>
</div>
<!-- Notes -->
<div v-else-if="activeTab === 'notes'">
<template v-if="notes.length !== 0">
<div v-for="note in notes" :key="note.id" class="pb3 cf">
<div class="fl w-10">
<avatar :contact="note.contact" :clickable="true" />
</div>
<div class="pl3 fl w-90">
<svg id="Layer_1" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" width="15px" height="15px" viewBox="0 0 512 512" enable-background="new 0 0 512 512"
xml:space="preserve"
>
<g>
<path fill="none" d="M397.997,102.5H383.5v18.686c0,6.921-5.582,12.53-12.5,12.53s-12.5-5.608-12.5-12.53V102.5h-17v18.686
c0,6.921-5.582,12.53-12.5,12.53c-6.916,0-12.5-5.608-12.5-12.53V102.5h-117v18.686c0,6.921-5.581,12.53-12.5,12.53
c-6.919,0-12.5-5.608-12.5-12.53V102.5h-17v18.686c0,6.921-5.581,12.53-12.5,12.53s-12.5-5.608-12.5-12.53V102.5h-14.321
c-4.613,0-10.679,1.544-10.679,6.157v325.76c0,4.612,6.065,11.083,10.679,11.083h279.818c4.613,0,6.503-6.471,6.503-11.083v-325.76
C404.5,104.044,402.611,102.5,397.997,102.5z M383.5,386.5h-251v-16h251V386.5z M383.5,345.5h-251v-17h251V345.5z M383.5,303.5
h-251v-17h251V303.5z M383.5,261.5h-251v-17h251V261.5z M383.5,219.5h-251v-17h251V219.5z"
/>
<rect x="132.5" y="244.5" width="251" height="17" />
<rect x="132.5" y="202.5" width="251" height="17" />
<rect x="132.5" y="286.5" width="251" height="17" />
<rect x="132.5" y="328.5" width="251" height="17" />
<rect x="132.5" y="370.5" width="251" height="16" />
<path d="M397.997,85.5H383.5V62.717c0-6.919-5.582-12.529-12.5-12.529s-12.5,5.61-12.5,12.529V85.5h-17V62.717
c0-6.919-5.582-12.529-12.5-12.529c-6.916,0-12.5,5.61-12.5,12.529V85.5h-117V62.717c0-6.919-5.581-12.529-12.5-12.529
c-6.919,0-12.5,5.61-12.5,12.529V85.5h-17V62.717c0-6.919-5.581-12.529-12.5-12.529s-12.5,5.61-12.5,12.529V85.5h-14.321
c-13.838,0-27.679,9.318-27.679,23.157v325.76c0,13.839,13.84,28.083,27.679,28.083h279.818c13.839,0,23.503-14.244,23.503-28.083
v-325.76C421.5,94.818,411.836,85.5,397.997,85.5z M404.5,434.417c0,4.612-1.89,11.083-6.503,11.083H118.179
c-4.613,0-10.679-6.471-10.679-11.083v-325.76c0-4.613,6.065-6.157,10.679-6.157H132.5v18.686c0,6.921,5.581,12.53,12.5,12.53
s12.5-5.608,12.5-12.53V102.5h17v18.686c0,6.921,5.581,12.53,12.5,12.53c6.919,0,12.5-5.608,12.5-12.53V102.5h117v18.686
c0,6.921,5.584,12.53,12.5,12.53c6.918,0,12.5-5.608,12.5-12.53V102.5h17v18.686c0,6.921,5.582,12.53,12.5,12.53
s12.5-5.608,12.5-12.53V102.5h14.497c4.613,0,6.503,1.544,6.503,6.157V434.417z"
/>
</g>
</svg>
<span class="black-50 mr1 f6">
{{ note.created_at }}
</span>
<span class="mr1 black-50">
•
</span>
<a :href="'people/' + note.contact.id">
{{ note.name }}
</a>
<p v-html="compiledMarkdown(note.body)"></p>
</div>
</div>
</template>
<!-- Notes: Blank state -->
<div v-if="notes.length === 0" class="tc mt4 mb4">
<p>{{ $t('dashboard.notes_title') }}</p>
</div>
</div>
<!-- Debts -->
<div v-else-if="activeTab === 'debts'">
<ul v-if="debts.length !== 0">
<li v-for="debt in debts" :key="debt.id" class="pb2">
<span class="black-50 mr1 f6">
{{ debt.created_at | formatDate }}
</span>
<span class="mr1 black-50">
•
</span>
<a :href="'people/' + debt.contact.hash_id">
{{ debt.contact.first_name }}
</a>
<span class="mr1 black-50">
•
</span>
{{ debt.amount_with_currency }}
<span v-if="debt.in_debt === 'yes'" class="black-50 f6">
<span class="mr1 black-50">
•
</span>
{{ $t('dashboard.debts_you_owe') }}
</span>
</li>
</ul>
<!-- Debts: Blank state -->
<div v-else class="tc mt4 mb4">
<p>{{ $t('dashboard.tab_debts_blank') }}</p>
</div>
</div>
<!-- Tasks -->
<div v-if="activeTab === 'tasks'">
<ul class="tc mb3">
<li class="di mr4">
<span :class="[contactRelatedTasksView === true ? 'b' : 'pointer']" @click.prevent="contactRelatedTasksView = true">
{{ $t('dashboard.tasks_tab_your_contacts') }} ({{ contactRelated(tasks).length }})
</span>
</li>
<li class="di">
<span :class="[contactRelatedTasksView === true ? 'pointer' : 'b']" @click.prevent="contactRelatedTasksView = false">
{{ $t('dashboard.tasks_tab_your_tasks') }} ({{ customNotCompleted(tasks).length }})
</span>
</li>
</ul>
<!-- Tasks: Create task -->
<div v-show="taskAddMode" class="br3 pa2 ba b--gray-monica mb3">
<div class="flex items-center mb2">
<input type="checkbox" disabled class="di mr2 pb2" />
<input v-model="newTask.title" type="text" class="bt-0 br-0 bl-0 w-100 di bb b--gray-monica pt2 pb2" :placeholder="$t('dashboard.tasks_add_task_placeholder')" @keyup.enter="saveTask()"
@keyup.esc="taskAddMode = false"
/>
<a class="pointer" href="" @click.prevent="taskAddMode = false; newTask.title=''">
{{ $t('app.cancel') }}
</a>
</div>
<div class="f7 relative" style="left: 20px;">
<span v-html="$t('dashboard.tasks_add_note')"></span>
</div>
</div>
<!-- Tasks: list of contact related tasks -->
<div v-if="contactRelated(tasks).length !== 0 && contactRelatedTasksView">
<ul>
<li v-for="task in contactRelated(tasks)" :key="task.id" class="pb0 mb2">
<form-checkbox
v-model.lazy="task.completed"
:name="'todo'"
:dclass="'mr1'"
@change="updateTask(task)"
>
{{ task.title }}
</form-checkbox>
<span class="black-50 mr1 f7">
<a :href="'people/' + task.contact.hash_id">
{{ task.contact.first_name }}
</a>
</span>
</li>
</ul>
</div>
<!-- Tasks: list of non contact related tasks -->
<div v-if="!contactRelatedTasksView">
<!-- Your tasks: Blank state -->
<div v-if="customNotCompleted(tasks).length === 0" class="tc mt4 mb4">
<p class="mb4">
<a v-show="!taskAddMode" class="btn pointer" href="" @click.prevent="taskAddMode = true">
{{ $t('dashboard.task_add_cta') }}
</a>
</p>
<img src="img/dashboard/blank_your_tasks.svg" :alt="$t('dashboard.tasks_tab_your_tasks')" />
</div>
<!-- Add a task -->
<p v-if="customNotCompleted(tasks).length !== 0">
<a v-show="!taskAddMode" class="pointer" href="" @click.prevent="taskAddMode = true">
{{ $t('dashboard.task_add_cta') }}
</a>
</p>
<!-- Actual list -->
<ul v-if="customNotCompleted(tasks).length !== 0">
<li v-for="task in customNotCompleted(tasks)" :key="task.id" class="pb0 mb2" @mouseover="showTaskAction = task.id" @mouseleave="showTaskAction = 0; confirmDestroyTask = 0">
<form-checkbox
v-model.lazy="task.completed"
:name="'todo'"
:dclass="'mr1'"
@change="updateTask(task)"
>
{{ task.title }}
</form-checkbox>
<a v-show="showTaskAction === task.id" class="pointer mr1" href="" @click.prevent="confirmDestroyTask = task.id">
{{ $t('app.delete') }}
</a>
<ul v-show="confirmDestroyTask === task.id" class="di">
<li class="di">
<a class="pointer mr1" href="" @click.prevent="confirmDestroyTask = 0">
{{ $t('app.cancel') }}
</a>
</li>
<li class="di">
<a class="pointer red" href="" @click.prevent="destroyTask(task)">
{{ $t('app.delete_confirm') }}
</a>
</li>
</ul>
</li>
</ul>
<ul v-if="customCompleted(tasks).length !== 0 && !contactRelatedTasksView">
<li v-for="task in customCompleted(tasks)" :key="task.id" class="pb0 mb0 f6" @mouseover="showTaskAction = task.id" @mouseleave="showTaskAction = 0; confirmDestroyTask = 0">
<form-checkbox
v-model.lazy="task.completed"
:name="'todo'"
:dclass="'mb1 mr1'"
@change="updateTask(task)"
>
{{ task.title }}
</form-checkbox>
<a v-show="showTaskAction === task.id" class="pointer mr1" href="" @click.prevent="confirmDestroyTask = task.id">
{{ $t('app.delete') }}
</a>
<ul v-show="confirmDestroyTask === task.id" class="di">
<li class="di">
<a class="pointer mr1" href="" @click.prevent="confirmDestroyTask = 0">
{{ $t('app.cancel') }}
</a>
</li>
<li class="di">
<a class="pointer red" href="" @click.prevent="destroyTask(task)">
{{ $t('app.delete_confirm') }}
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import Avatar from '../partials/Avatar.vue';
export default {
components: {
Avatar,
},
props: {
defaultActiveTab: {
type: String,
default: 'calls',
},
},
data() {
return {
activeTab: '',
callsAlreadyLoaded: false,
notesAlreadyLoaded: false,
debtsAlreadyLoaded: false,
tasksAlreadyLoaded: false,
calls: [],
notes: [],
debts: [],
tasks: [],
taskAddMode: false,
contactRelatedTasksView: true,
confirmDestroyTask: 0,
showTaskAction: 0,
newTask: {
id: 0,
title: '',
description: ''
},
};
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.setActiveTab(this.defaultActiveTab);
},
setActiveTab(view) {
this.activeTab = view;
this.saveTab(view);
switch (view) {
case 'calls':
if (! this.callsAlreadyLoaded) {
this.getCalls();
this.callsAlreadyLoaded = true;
}
break;
case 'notes':
if (! this.notesAlreadyLoaded) {
this.getNotes();
this.notesAlreadyLoaded = true;
}
break;
case 'debts':
if (! this.debtsAlreadyLoaded) {
this.getDebts();
this.debtsAlreadyLoaded = true;
}
break;
case 'tasks':
if (! this.tasksAlreadyLoaded) {
this.getTasks();
this.tasksAlreadyLoaded = true;
}
break;
}
},
saveTab(view) {
axios.post('dashboard/setTab', {'tab':view});
},
getCalls() {
axios.get('dashboard/calls')
.then(response => {
this.calls = response.data;
});
},
getNotes() {
axios.get('dashboard/notes')
.then(response => {
this.notes = response.data;
});
},
getDebts() {
axios.get('dashboard/debts')
.then(response => {
this.debts = response.data;
});
},
getTasks() {
axios.get('tasks')
.then(response => {
this.tasks = response.data.data;
});
},
// All the custom tasks not yet completed
customNotCompleted: function (tasks) {
return tasks.filter(function (task) {
return task.contact === null && task.completed === false;
});
},
// All the custom completed tasks
customCompleted: function (tasks) {
return tasks.filter(function (task) {
return task.contact === null && task.completed === true;
});
},
// All the contact related tasks
contactRelated: function (tasks) {
return tasks.filter(function (task) {
return task.contact !== null && !task.completed;
});
},
updateTask(task) {
axios.put('tasks/' + task.id, task)
.then(response => {
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
saveTask() {
axios.post('tasks', this.newTask)
.then(response => {
this.newTask.title = '';
this.taskAddMode = false;
this.getTasks();
this.$notify({
group: 'main',
title: this.$t('app.default_save_success'),
text: '',
type: 'success'
});
});
},
destroyTask(task) {
axios.delete('tasks/' + task.id)
.then(response => {
this.tasks.splice(this.tasks.indexOf(task), 1);
});
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
},
}
};
</script>

View File

@@ -0,0 +1,217 @@
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity .4s
}
</style>
<template>
<div class="mw9 center">
<!-- Left sidebar -->
<div :class="[dirltr ? 'fl' : 'fr']" class="w-70-ns w-100 pa2">
<!-- Filters -->
<div class="filter mb-4">
<div class="d-flex pb-2">
<div class="dt">
<label for="start-date">{{ $t('journal.start_date') }}:</label>
<input id="start-date" v-model="startDate" type="date" class="form-control" />
</div>
<div class="dt pl-2">
<label for="end-date py-2">{{ $t('journal.end_date') }}:</label>
<input id="end-date" v-model="endDate" type="date" class="form-control" />
</div>
<div class="dt pl-2">
<label for="per-page">{{ $t('journal.per_page') }}:</label>
<input id="per-page" v-model="perPage" type="number" class="form-control" />
</div>
<div class="dt pl-2">
<label for="sort-order">{{ $t('journal.sort_order') }} :</label>
<select id="sort-order" v-model="sortOrder" class="form-control">
<option value="asc">
{{ $t('journal.ascending') }}
</option>
<option value="desc">
{{ $t('journal.descending') }}
</option>
</select>
</div>
</div>
<button class="btn btn-primary" @click="getEntries">
{{ $t('journal.apply_filter') }}
</button>
</div>
<!-- How was your day -->
<journal-rate-day @hasRated="hasRated" />
<!-- Logs -->
<div v-if="journalEntries.data" v-cy-name="'journal-entries-body'" v-cy-items="journalEntries.data.map(j => j.id)"
:cy-object-items="journalEntries.data.map(j => j.object.id)"
>
<div v-for="journalEntry in journalEntries.data" :key="journalEntry.id"
v-cy-name="'entry-body-' + journalEntry.id" class="cf"
>
<journal-content-rate v-if="journalEntry.journalable_type === 'App\\Models\\Journal\\Day'"
:journal-entry="journalEntry" @deleteJournalEntry="deleteJournalEntry"
/>
<journal-content-activity v-else-if="journalEntry.journalable_type === 'App\\Models\\Account\\Activity'"
:journal-entry="journalEntry"
/>
<journal-content-entry v-else-if="journalEntry.journalable_type === 'App\\Models\\Journal\\Entry'"
:journal-entry="journalEntry" @deleteJournalEntry="deleteJournalEntry"
/>
</div>
</div>
<div v-if="(journalEntries.per_page * journalEntries.current_page) <= journalEntries.total"
class="br3 ba b--gray-monica bg-white pr3 pb3 pt3 mb3 tc"
>
<p class="mb0 pointer" @click="loadMore()">
<span v-if="!loadingMore">
{{ $t('app.load_more') }}
</span>
<span v-else class="black-50">
{{ $t('app.loading') }}
</span>
</p>
</div>
<div v-if="journalEntries.total === 0" v-cy-name="'journal-blank-state'"
class="br3 ba b--gray-monica bg-white pr3 pb3 pt3 mb3 tc"
>
<div class="tc mb4">
<img src="img/journal/blank.svg" :alt="$t('journal.journal_empty')" />
</div>
<h3>
{{ $t('journal.journal_blank_cta') }}
</h3>
<p>{{ $t('journal.journal_blank_description') }}</p>
</div>
</div>
<!-- Right sidebar -->
<div :class="[dirltr ? 'fl' : 'fr']" class="w-30-ns w-100 pa2">
<a v-cy-name="'add-entry-button'" href="journal/add" class="btn btn-primary w-100 mb4">
{{ $t('journal.journal_add') }}
</a>
<p>{{ $t('journal.journal_description') }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
journalEntries: [],
day: {
rate: ''
},
showSadSmileyColor: false,
showHappySmileyColor: false,
loadingMore: false,
startDate: '',
endDate: '',
sortBy: 'created_at', // Specify the field to sort by (e.g., 'created_at', 'updated_at')
sortOrder: 'desc', // Specify the sort order ('asc' or 'desc')
perPage: 30, // Specify the number of entries per page
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
hasMorePage: function () {
var total = this.journalEntries.per_page * this.journalEntries.current_page;
if (total >= this.journalEntries.total) {
return true;
}
return false;
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getEntries();
},
getEntries() {
axios.get('journal/entries', {
params: {
start_date: this.startDate,
end_date: this.endDate,
per_page: this.perPage,
sort_order: this.sortOrder,
sort_by: this.sortBy,
},
})
.then(response => {
this.journalEntries = response.data;
this.journalEntries.current_page = response.data.current_page;
this.journalEntries.next_page_url = response.data.next_page_url;
this.journalEntries.per_page = response.data.per_page;
this.journalEntries.prev_page_url = response.data.prev_page_url;
this.journalEntries.total = response.data.total;
});
},
// This event is omited from the child component
deleteJournalEntry: function ($journalEntryId) {
// check if the deleted entry date is today. If that's the case
// we need to put back the Rate box. This is only necessary if
// the user does all his actions on the same page without ever
// reloading the page.
this.journalEntries.data.filter(function (obj) {
return obj.id === $journalEntryId;
});
// Filter out the array without the deleted Journal Entry
this.journalEntries.data = this.journalEntries.data.filter(function (element) {
return element.id !== $journalEntryId;
});
},
hasRated: function (journalObject) {
this.journalEntries.data.unshift(journalObject);
},
loadMore() {
this.loadingMore = true;
axios.get('journal/entries?page=' + (this.journalEntries.current_page + 1),{
params: {
start_date: this.startDate,
end_date: this.endDate,
per_page: this.perPage,
sort_order: this.sortOrder,
sort_by: this.sortBy,
},
})
.then(response => {
this.journalEntries.current_page = response.data.current_page;
this.journalEntries.next_page_url = response.data.next_page_url;
this.journalEntries.per_page = response.data.per_page;
this.journalEntries.prev_page_url = response.data.prev_page_url;
this.journalEntries.total = response.data.total;
for (var j of response.data.data) {
this.journalEntries.data.push(j);
}
this.loadingMore = false;
});
},
}
};
</script>

View File

@@ -0,0 +1,250 @@
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity .4s
}
.selected {
border: 2px solid #0981e8;
border-radius: 50%;
padding: 2px;
}
</style>
<template>
<div v-if="hasRated !== true" class="br3 ba b--gray-monica bg-white pa3 mb4">
<transition-group name="fade" mode="out-in">
<div v-if="hasRated === 'notYet' || hasRated === 'addComment'" :key="'rate'">
<!-- RATE BOX -->
<div class="flex items-center">
<div class="fl w-70 f3 pl2">
{{ $t('journal.journal_rate') }}
</div>
<div class="w-30">
<div class="flex items-center h-100">
<div class="flex-none w-100" :class="[ dirltr ? 'tr' : 'tl' ]">
<!-- sad smiley monochrome -->
<svg v-if="!showSadSmileyColor" v-cy-name="'sad-reaction-button'" width="42px" height="41px" viewBox="0 0 42 41"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 1 }"
@click="showComment(1)" @mouseover="showSadSmileyColor = true"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-695.000000, -165.000000)">
<g id="Group" transform="translate(695.000000, 165.000000)">
<path id="Fill-87" d="M41.9467391,20.5475529 C41.9467391,31.8129081 32.5673478,40.9454502 20.9978696,40.9454502 C9.42869565,40.9454502 0.0496086957,31.8129081 0.0496086957,20.5475529 C0.0496086957,9.28219757 9.42869565,0.149951854 20.9978696,0.149951854 C32.5673478,0.149951854 41.9467391,9.28219757 41.9467391,20.5475529" fill="#E5E5E5" />
<path id="Fill-88" d="M36.4447391,28.5705697 C36.4447391,30.1432861 35.1342174,31.4187659 33.5184348,31.4187659 C31.9026522,31.4187659 30.5927391,30.1432861 30.5927391,28.5705697 C30.5927391,26.996668 33.5184348,21.6244798 33.5184348,21.6244798 C33.5184348,21.6244798 36.4447391,26.996668 36.4447391,28.5705697" fill="#399EAF" />
<path id="Fill-89" d="M32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 L32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 C32.3226522,15.4942346 32.3220435,15.6601893 32.284913,15.8954892 C32.2575217,16.071816 32.2094348,16.2857789 32.1336522,16.4935185 C32.0767391,16.6496937 32.0043043,16.8014236 31.9184783,16.9350764 C31.7876087,17.1365927 31.6351304,17.2927678 31.4339565,17.4077507 C31.2312609,17.5212518 30.9661739,17.6095634 30.5346087,17.6116378 C30.282913,17.6113414 30.0866087,17.5793359 29.9283478,17.532513 C29.6532174,17.4486467 29.4806522,17.3265515 29.3254348,17.1608932 C29.2113043,17.0367236 29.1136087,16.8844009 29.035087,16.7154828 C28.9163913,16.4629947 28.8448696,16.1761303 28.8095652,15.9470537 C28.7733478,15.717977 28.7715217,15.5481699 28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 C28.784913,14.9637726 28.3192609,14.4887274 27.7312609,14.4753918 C27.1429565,14.4623525 26.655087,14.9157643 26.6416957,15.488604 L26.6413913,15.528611 C26.6413913,15.6296655 26.6468696,15.9049723 26.7065217,16.2780739 C26.7515652,16.5581223 26.8267391,16.8935877 26.9612609,17.2533536 C27.0623043,17.5227335 27.1968261,17.8063381 27.3812609,18.0863866 C27.656087,18.5051256 28.0526522,18.9188268 28.5907391,19.2172488 C29.1276087,19.5171525 29.7916957,19.6878486 30.5346087,19.6860706 C30.9835217,19.6860706 31.4026087,19.6256157 31.7836522,19.5094474 C32.4526087,19.3082274 32.9897826,18.9303843 33.368087,18.5045329 C33.6538696,18.1850703 33.8583913,17.8421962 34.0081304,17.5129541 C34.2324348,17.0186464 34.3377391,16.5521954 34.3937391,16.1906514 C34.4491304,15.8291074 34.4527826,15.5706923 34.453087,15.4939383 L34.453087,15.4788246 C34.447913,14.9059848 33.9667391,14.4454607 33.3784348,14.4504986 C32.7901304,14.4555365 32.3174783,14.924062 32.3226522,15.4969018" fill="#36301D" />
<path id="Fill-90" d="M13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 L13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 C13.2248261,15.4942346 13.2242174,15.6601893 13.187087,15.8954892 C13.1596957,16.071816 13.1116087,16.2857789 13.0358261,16.4935185 C12.978913,16.6496937 12.9064783,16.8014236 12.8206522,16.9350764 C12.6897826,17.1365927 12.5373043,17.2927678 12.3361304,17.4077507 C12.1334348,17.5212518 11.8683478,17.6095634 11.4367826,17.6116378 C11.185087,17.6113414 10.9887826,17.5793359 10.8305217,17.532513 C10.5553913,17.4486467 10.3828261,17.3265515 10.2276087,17.1608932 C10.1134783,17.0367236 10.0157826,16.8844009 9.93726087,16.7154828 C9.81856522,16.4629947 9.74704348,16.1761303 9.71173913,15.9470537 C9.67552174,15.717977 9.67369565,15.5481699 9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 C9.68708696,14.9637726 9.22143478,14.4887274 8.63343478,14.4753918 C8.04513043,14.4623525 7.55726087,14.9157643 7.54386957,15.488604 L7.54356522,15.528611 C7.54356522,15.6296655 7.54904348,15.9049723 7.60869565,16.2780739 C7.65373913,16.5581223 7.72891304,16.8935877 7.86343478,17.2533536 C7.96447826,17.5227335 8.099,17.8063381 8.28343478,18.0863866 C8.55826087,18.5051256 8.95482609,18.9188268 9.49291304,19.2172488 C10.0297826,19.5171525 10.6938696,19.6878486 11.4367826,19.6860706 C11.8856957,19.6860706 12.3047826,19.6256157 12.6858261,19.5094474 C13.3547826,19.3082274 13.8919565,18.9303843 14.2702609,18.5045329 C14.5560435,18.1850703 14.7605652,17.8421962 14.9103043,17.5129541 C15.1346087,17.0186464 15.239913,16.5521954 15.295913,16.1906514 C15.3513043,15.8291074 15.3549565,15.5706923 15.3552609,15.4939383 L15.3552609,15.4788246 C15.350087,14.9059848 14.868913,14.4454607 14.2806087,14.4504986 C13.6923043,14.4555365 13.2196522,14.924062 13.2248261,15.4969018" fill="#36301D" />
<path id="Fill-91" d="M14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 L14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 C14.5359565,28.0605556 14.9410435,26.8330841 15.946913,25.6951096 C16.4493913,25.1240479 17.0946087,24.5740268 17.9184783,24.1653636 C18.7438696,23.757293 19.7503478,23.4816898 21.0316522,23.4802081 C22.3558696,23.4816898 23.3818261,23.7729994 24.2145217,24.1988509 C24.8381304,24.5192025 25.353087,24.9201608 25.7794783,25.3477903 C26.418913,25.9884936 26.8541304,26.6917263 27.1222609,27.2287081 C27.2564783,27.4969026 27.349,27.7227194 27.4062174,27.8756348 C27.4348261,27.9520924 27.4546087,28.0101765 27.4661739,28.0463309 L27.4780435,28.0836707 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 C27.6347826,28.6369516 28.2215652,28.9614521 28.7885652,28.8091295 C29.3555652,28.6568069 29.6885217,28.0857452 29.5323913,27.5333533 C29.505913,27.4509687 29.0932174,25.9884936 27.8265217,24.4703052 C27.1925652,23.7134336 26.3367391,22.9435227 25.2082174,22.3638669 C24.0812174,21.7836184 22.6845652,21.4042936 21.0316522,21.4057753 C19.3775217,21.4042936 17.9766087,21.7868783 16.8444348,22.3709792 C15.9943913,22.8077955 15.2953043,23.3533713 14.7292174,23.9188024 C13.8797826,24.7681345 13.3249565,25.6610296 12.9749565,26.3482595 C12.6255652,27.0357858 12.4806957,27.5200177 12.467,27.5665443 C12.305087,28.117158 12.6328696,28.6911832 13.1983478,28.8488401 C13.7641304,29.0062006 14.3536522,28.6870343 14.5152609,28.1364206" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- sad smiley color -->
<svg v-if="showSadSmileyColor" v-cy-name="'sad-reaction-button'" width="42px" height="41px" viewBox="0 0 42 41"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 1 }"
@click="showComment(1)" @mouseleave="showSadSmileyColor = false"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-695.000000, -345.000000)">
<g id="Group" transform="translate(695.000000, 345.000000)">
<path id="Fill-87" d="M41.9467391,20.5475529 C41.9467391,31.8129081 32.5673478,40.9454502 20.9978696,40.9454502 C9.42869565,40.9454502 0.0496086957,31.8129081 0.0496086957,20.5475529 C0.0496086957,9.28219757 9.42869565,0.149951854 20.9978696,0.149951854 C32.5673478,0.149951854 41.9467391,9.28219757 41.9467391,20.5475529" fill="#FF7272" />
<path id="Fill-88" d="M36.4447391,28.5705697 C36.4447391,30.1432861 35.1342174,31.4187659 33.5184348,31.4187659 C31.9026522,31.4187659 30.5927391,30.1432861 30.5927391,28.5705697 C30.5927391,26.996668 33.5184348,21.6244798 33.5184348,21.6244798 C33.5184348,21.6244798 36.4447391,26.996668 36.4447391,28.5705697" fill="#D0021B" />
<path id="Fill-89" d="M32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 L32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 C32.3226522,15.4942346 32.3220435,15.6601893 32.284913,15.8954892 C32.2575217,16.071816 32.2094348,16.2857789 32.1336522,16.4935185 C32.0767391,16.6496937 32.0043043,16.8014236 31.9184783,16.9350764 C31.7876087,17.1365927 31.6351304,17.2927678 31.4339565,17.4077507 C31.2312609,17.5212518 30.9661739,17.6095634 30.5346087,17.6116378 C30.282913,17.6113414 30.0866087,17.5793359 29.9283478,17.532513 C29.6532174,17.4486467 29.4806522,17.3265515 29.3254348,17.1608932 C29.2113043,17.0367236 29.1136087,16.8844009 29.035087,16.7154828 C28.9163913,16.4629947 28.8448696,16.1761303 28.8095652,15.9470537 C28.7733478,15.717977 28.7715217,15.5481699 28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 C28.784913,14.9637726 28.3192609,14.4887274 27.7312609,14.4753918 C27.1429565,14.4623525 26.655087,14.9157643 26.6416957,15.488604 L26.6413913,15.528611 C26.6413913,15.6296655 26.6468696,15.9049723 26.7065217,16.2780739 C26.7515652,16.5581223 26.8267391,16.8935877 26.9612609,17.2533536 C27.0623043,17.5227335 27.1968261,17.8063381 27.3812609,18.0863866 C27.656087,18.5051256 28.0526522,18.9188268 28.5907391,19.2172488 C29.1276087,19.5171525 29.7916957,19.6878486 30.5346087,19.6860706 C30.9835217,19.6860706 31.4026087,19.6256157 31.7836522,19.5094474 C32.4526087,19.3082274 32.9897826,18.9303843 33.368087,18.5045329 C33.6538696,18.1850703 33.8583913,17.8421962 34.0081304,17.5129541 C34.2324348,17.0186464 34.3377391,16.5521954 34.3937391,16.1906514 C34.4491304,15.8291074 34.4527826,15.5706923 34.453087,15.4939383 L34.453087,15.4788246 C34.447913,14.9059848 33.9667391,14.4454607 33.3784348,14.4504986 C32.7901304,14.4555365 32.3174783,14.924062 32.3226522,15.4969018" fill="#36301D" />
<path id="Fill-90" d="M13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 L13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 C13.2248261,15.4942346 13.2242174,15.6601893 13.187087,15.8954892 C13.1596957,16.071816 13.1116087,16.2857789 13.0358261,16.4935185 C12.978913,16.6496937 12.9064783,16.8014236 12.8206522,16.9350764 C12.6897826,17.1365927 12.5373043,17.2927678 12.3361304,17.4077507 C12.1334348,17.5212518 11.8683478,17.6095634 11.4367826,17.6116378 C11.185087,17.6113414 10.9887826,17.5793359 10.8305217,17.532513 C10.5553913,17.4486467 10.3828261,17.3265515 10.2276087,17.1608932 C10.1134783,17.0367236 10.0157826,16.8844009 9.93726087,16.7154828 C9.81856522,16.4629947 9.74704348,16.1761303 9.71173913,15.9470537 C9.67552174,15.717977 9.67369565,15.5481699 9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 C9.68708696,14.9637726 9.22143478,14.4887274 8.63343478,14.4753918 C8.04513043,14.4623525 7.55726087,14.9157643 7.54386957,15.488604 L7.54356522,15.528611 C7.54356522,15.6296655 7.54904348,15.9049723 7.60869565,16.2780739 C7.65373913,16.5581223 7.72891304,16.8935877 7.86343478,17.2533536 C7.96447826,17.5227335 8.099,17.8063381 8.28343478,18.0863866 C8.55826087,18.5051256 8.95482609,18.9188268 9.49291304,19.2172488 C10.0297826,19.5171525 10.6938696,19.6878486 11.4367826,19.6860706 C11.8856957,19.6860706 12.3047826,19.6256157 12.6858261,19.5094474 C13.3547826,19.3082274 13.8919565,18.9303843 14.2702609,18.5045329 C14.5560435,18.1850703 14.7605652,17.8421962 14.9103043,17.5129541 C15.1346087,17.0186464 15.239913,16.5521954 15.295913,16.1906514 C15.3513043,15.8291074 15.3549565,15.5706923 15.3552609,15.4939383 L15.3552609,15.4788246 C15.350087,14.9059848 14.868913,14.4454607 14.2806087,14.4504986 C13.6923043,14.4555365 13.2196522,14.924062 13.2248261,15.4969018" fill="#36301D" />
<path id="Fill-91" d="M14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 L14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 C14.5359565,28.0605556 14.9410435,26.8330841 15.946913,25.6951096 C16.4493913,25.1240479 17.0946087,24.5740268 17.9184783,24.1653636 C18.7438696,23.757293 19.7503478,23.4816898 21.0316522,23.4802081 C22.3558696,23.4816898 23.3818261,23.7729994 24.2145217,24.1988509 C24.8381304,24.5192025 25.353087,24.9201608 25.7794783,25.3477903 C26.418913,25.9884936 26.8541304,26.6917263 27.1222609,27.2287081 C27.2564783,27.4969026 27.349,27.7227194 27.4062174,27.8756348 C27.4348261,27.9520924 27.4546087,28.0101765 27.4661739,28.0463309 L27.4780435,28.0836707 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 C27.6347826,28.6369516 28.2215652,28.9614521 28.7885652,28.8091295 C29.3555652,28.6568069 29.6885217,28.0857452 29.5323913,27.5333533 C29.505913,27.4509687 29.0932174,25.9884936 27.8265217,24.4703052 C27.1925652,23.7134336 26.3367391,22.9435227 25.2082174,22.3638669 C24.0812174,21.7836184 22.6845652,21.4042936 21.0316522,21.4057753 C19.3775217,21.4042936 17.9766087,21.7868783 16.8444348,22.3709792 C15.9943913,22.8077955 15.2953043,23.3533713 14.7292174,23.9188024 C13.8797826,24.7681345 13.3249565,25.6610296 12.9749565,26.3482595 C12.6255652,27.0357858 12.4806957,27.5200177 12.467,27.5665443 C12.305087,28.117158 12.6328696,28.6911832 13.1983478,28.8488401 C13.7641304,29.0062006 14.3536522,28.6870343 14.5152609,28.1364206" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- mediocre day monochrome -->
<svg v-if="!showMediocreSmileyColor" v-cy-name="'medium-reaction-button'" width="42px" height="41px" viewBox="0 0 42 41"
version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 2 }" @click="showComment(2)" @mouseover="showMediocreSmileyColor = true"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-754.000000, -165.000000)">
<g id="Group-2" transform="translate(754.000000, 165.000000)">
<path id="Fill-96" d="M41.8643309,20.5475529 C41.8643309,31.8129081 32.5521151,40.9454502 21.0661727,40.9454502 C9.57992806,40.9454502 0.268618705,31.8129081 0.268618705,20.5475529 C0.268618705,9.28219757 9.57992806,0.149951854 21.0661727,0.149951854 C32.5521151,0.149951854 41.8643309,9.28219757 41.8643309,20.5475529" fill="#E5E5E5" />
<path id="Fill-97" d="M11.5989496,21.8043628 L11.5989496,21.8043628 C10.7680144,21.8043628 10.0881583,21.1375808 10.0881583,20.3226251 L10.0881583,16.7664546 C10.0881583,15.9514989 10.7680144,15.2847169 11.5989496,15.2847169 C12.4298849,15.2847169 13.109741,15.9514989 13.109741,16.7664546 L13.109741,20.3226251 C13.109741,21.1375808 12.4298849,21.8043628 11.5989496,21.8043628" fill="#36301D" />
<path id="Fill-98" d="M30.534,21.8043628 L30.534,21.8043628 C29.7030647,21.8043628 29.0232086,21.1375808 29.0232086,20.3226251 L29.0232086,16.7664546 C29.0232086,15.9514989 29.7030647,15.2847169 30.534,15.2847169 C31.3649353,15.2847169 32.0447914,15.9514989 32.0447914,16.7664546 L32.0447914,20.3226251 C32.0447914,21.1375808 31.3649353,21.8043628 30.534,21.8043628" fill="#36301D" />
<path id="Fill-99" d="M9.15690647,27.2639734 L32.9760432,27.2639734 C33.4767194,27.2639734 33.882518,26.8659787 33.882518,26.3749308 C33.882518,25.883883 33.4767194,25.4858882 32.9760432,25.4858882 L9.15690647,25.4858882 C8.65623022,25.4858882 8.25043165,25.883883 8.25043165,26.3749308 C8.25043165,26.8659787 8.65623022,27.2639734 9.15690647,27.2639734" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- mediocre day color -->
<svg v-if="showMediocreSmileyColor" v-cy-name="'medium-reaction-button'" width="42px" height="41px" viewBox="0 0 42 41"
version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 2 }" @click="showComment(2)" @mouseleave="showMediocreSmileyColor = false"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-754.000000, -165.000000)">
<g id="Group-2" transform="translate(754.000000, 165.000000)">
<path id="Fill-96" d="M41.8643309,20.5475529 C41.8643309,31.8129081 32.5521151,40.9454502 21.0661727,40.9454502 C9.57992806,40.9454502 0.268618705,31.8129081 0.268618705,20.5475529 C0.268618705,9.28219757 9.57992806,0.149951854 21.0661727,0.149951854 C32.5521151,0.149951854 41.8643309,9.28219757 41.8643309,20.5475529" fill="#FFEA7C" />
<path id="Fill-97" d="M11.5989496,21.8043628 L11.5989496,21.8043628 C10.7680144,21.8043628 10.0881583,21.1375808 10.0881583,20.3226251 L10.0881583,16.7664546 C10.0881583,15.9514989 10.7680144,15.2847169 11.5989496,15.2847169 C12.4298849,15.2847169 13.109741,15.9514989 13.109741,16.7664546 L13.109741,20.3226251 C13.109741,21.1375808 12.4298849,21.8043628 11.5989496,21.8043628" fill="#36301D" />
<path id="Fill-98" d="M30.534,21.8043628 L30.534,21.8043628 C29.7030647,21.8043628 29.0232086,21.1375808 29.0232086,20.3226251 L29.0232086,16.7664546 C29.0232086,15.9514989 29.7030647,15.2847169 30.534,15.2847169 C31.3649353,15.2847169 32.0447914,15.9514989 32.0447914,16.7664546 L32.0447914,20.3226251 C32.0447914,21.1375808 31.3649353,21.8043628 30.534,21.8043628" fill="#36301D" />
<path id="Fill-99" d="M9.15690647,27.2639734 L32.9760432,27.2639734 C33.4767194,27.2639734 33.882518,26.8659787 33.882518,26.3749308 C33.882518,25.883883 33.4767194,25.4858882 32.9760432,25.4858882 L9.15690647,25.4858882 C8.65623022,25.4858882 8.25043165,25.883883 8.25043165,26.3749308 C8.25043165,26.8659787 8.65623022,27.2639734 9.15690647,27.2639734" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- happy day monochrome -->
<svg v-if="!showHappySmileyColor" v-cy-name="'happy-reaction-button'" width="42px" height="42px" viewBox="0 0 42 42"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 3 }"
@click="showComment(3)" @mouseover="showHappySmileyColor = true"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-814.000000, -165.000000)">
<g id="Group-3" transform="translate(814.000000, 165.000000)">
<path id="Fill-11" d="M42,20.5961538 C42,31.9710023 32.5972745,41.1923077 20.9996949,41.1923077 C9.40181023,41.1923077 0,31.9710023 0,20.5961538 C0,9.22130542 9.40181023,0 20.9996949,0 C32.5972745,0 42,9.22130542 42,20.5961538" fill="#E5E5E5" />
<path id="Fill-12" d="M11.3076923,21 L11.3076923,21 C10.4192308,21 9.69230769,20.3391608 9.69230769,19.5314685 L9.69230769,16.006993 C9.69230769,15.1993007 10.4192308,14.5384615 11.3076923,14.5384615 C12.1961538,14.5384615 12.9230769,15.1993007 12.9230769,16.006993 L12.9230769,19.5314685 C12.9230769,20.3391608 12.1961538,21 11.3076923,21" fill="#36301D" />
<path id="Fill-13" d="M30.6923077,21 L30.6923077,21 C29.8038462,21 29.0769231,20.3391608 29.0769231,19.5314685 L29.0769231,16.006993 C29.0769231,15.1993007 29.8038462,14.5384615 30.6923077,14.5384615 C31.5807692,14.5384615 32.3076923,15.1993007 32.3076923,16.006993 L32.3076923,19.5314685 C32.3076923,20.3391608 31.5807692,21 30.6923077,21" fill="#36301D" />
<path id="Fill-14" d="M27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0756973,24.0432687 C27.0404698,24.1114273 26.6683226,24.8097374 25.6979098,25.4916389 C24.7250883,26.1716472 23.1440652,26.864593 20.5631224,26.868064 C19.1874426,26.8677485 18.0993036,26.6692681 17.239993,26.3852739 C15.9516293,25.9577049 15.1687955,25.3417531 14.6996974,24.8539142 C14.4660516,24.6096793 14.3127969,24.3966836 14.222771,24.2556332 C14.1776075,24.1852658 14.1484018,24.1325691 14.1330462,24.1035385 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 C13.8828405,23.5383902 13.2565734,23.296364 12.7281606,23.5516432 C12.1997478,23.8069224 11.9688119,24.4629489 12.2123936,25.0170531 C12.2659876,25.1388551 12.7408064,26.1596563 14.0342887,27.154267 C14.6795244,27.6503102 15.5267914,28.1353091 16.6058977,28.491564 C17.6847028,28.84845 18.9935407,29.0772231 20.5631224,29.0769075 C23.7028881,29.0813252 25.8180447,28.1431978 27.1220651,27.1429072 C28.4293976,26.1448255 28.919873,25.1224465 28.9737681,25.0038001 C29.224576,24.4531669 29.0023717,23.7939849 28.4769698,23.5311325 C27.9518689,23.2682801 27.3228921,23.5014709 27.0720842,24.0517885" fill="#36301D" />
<path id="Fill-15" d="M7.26939206,25.8461538 C5.48457391,25.8461538 4.03846154,24.5805233 4.03846154,23.0190896 C4.03846154,21.457656 5.48457391,20.1923077 7.26939206,20.1923077 C9.05324247,20.1923077 10.5,21.457656 10.5,23.0190896 C10.5,24.5805233 9.05324247,25.8461538 7.26939206,25.8461538" fill="#EB9260" />
<path id="Fill-16" d="M37.1540074,25.8461538 C35.3691893,25.8461538 33.9230769,24.5805233 33.9230769,23.0190896 C33.9230769,21.457656 35.3691893,20.1923077 37.1540074,20.1923077 C38.9381804,20.1923077 40.3846154,21.457656 40.3846154,23.0190896 C40.3846154,24.5805233 38.9381804,25.8461538 37.1540074,25.8461538" fill="#EB9260" />
</g>
</g>
</g>
</svg>
<!-- happy day color -->
<svg v-if="showHappySmileyColor" v-cy-name="'happy-reaction-button'" width="42px" height="42px" viewBox="0 0 42 42"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
class="pointer" :class="{ selected: day.rate === 3 }"
@click="showComment(3)" @mouseleave="showHappySmileyColor = false"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-814.000000, -345.000000)">
<g id="Group-3" transform="translate(814.000000, 345.000000)">
<path id="Fill-11" d="M42,20.5961538 C42,31.9710023 32.5972745,41.1923077 20.9996949,41.1923077 C9.40181023,41.1923077 0,31.9710023 0,20.5961538 C0,9.22130542 9.40181023,0 20.9996949,0 C32.5972745,0 42,9.22130542 42,20.5961538" fill="#C4F1AF" />
<path id="Fill-12" d="M11.3076923,21 L11.3076923,21 C10.4192308,21 9.69230769,20.3391608 9.69230769,19.5314685 L9.69230769,16.006993 C9.69230769,15.1993007 10.4192308,14.5384615 11.3076923,14.5384615 C12.1961538,14.5384615 12.9230769,15.1993007 12.9230769,16.006993 L12.9230769,19.5314685 C12.9230769,20.3391608 12.1961538,21 11.3076923,21" fill="#36301D" />
<path id="Fill-13" d="M30.6923077,21 L30.6923077,21 C29.8038462,21 29.0769231,20.3391608 29.0769231,19.5314685 L29.0769231,16.006993 C29.0769231,15.1993007 29.8038462,14.5384615 30.6923077,14.5384615 C31.5807692,14.5384615 32.3076923,15.1993007 32.3076923,16.006993 L32.3076923,19.5314685 C32.3076923,20.3391608 31.5807692,21 30.6923077,21" fill="#36301D" />
<path id="Fill-14" d="M27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0756973,24.0432687 C27.0404698,24.1114273 26.6683226,24.8097374 25.6979098,25.4916389 C24.7250883,26.1716472 23.1440652,26.864593 20.5631224,26.868064 C19.1874426,26.8677485 18.0993036,26.6692681 17.239993,26.3852739 C15.9516293,25.9577049 15.1687955,25.3417531 14.6996974,24.8539142 C14.4660516,24.6096793 14.3127969,24.3966836 14.222771,24.2556332 C14.1776075,24.1852658 14.1484018,24.1325691 14.1330462,24.1035385 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 C13.8828405,23.5383902 13.2565734,23.296364 12.7281606,23.5516432 C12.1997478,23.8069224 11.9688119,24.4629489 12.2123936,25.0170531 C12.2659876,25.1388551 12.7408064,26.1596563 14.0342887,27.154267 C14.6795244,27.6503102 15.5267914,28.1353091 16.6058977,28.491564 C17.6847028,28.84845 18.9935407,29.0772231 20.5631224,29.0769075 C23.7028881,29.0813252 25.8180447,28.1431978 27.1220651,27.1429072 C28.4293976,26.1448255 28.919873,25.1224465 28.9737681,25.0038001 C29.224576,24.4531669 29.0023717,23.7939849 28.4769698,23.5311325 C27.9518689,23.2682801 27.3228921,23.5014709 27.0720842,24.0517885" fill="#36301D" />
<path id="Fill-15" d="M7.26939206,25.8461538 C5.48457391,25.8461538 4.03846154,24.5805233 4.03846154,23.0190896 C4.03846154,21.457656 5.48457391,20.1923077 7.26939206,20.1923077 C9.05324247,20.1923077 10.5,21.457656 10.5,23.0190896 C10.5,24.5805233 9.05324247,25.8461538 7.26939206,25.8461538" fill="#EB9260" />
<path id="Fill-16" d="M37.1540074,25.8461538 C35.3691893,25.8461538 33.9230769,24.5805233 33.9230769,23.0190896 C33.9230769,21.457656 35.3691893,20.1923077 37.1540074,20.1923077 C38.9381804,20.1923077 40.3846154,21.457656 40.3846154,23.0190896 C40.3846154,24.5805233 38.9381804,25.8461538 37.1540074,25.8461538" fill="#EB9260" />
</g>
</g>
</g>
</svg>
</div>
</div>
</div>
</div>
<!-- COMMENT BOX -->
<div v-if="hasRated === 'addComment'" :key="'comment'" v-cy-name="'comment'" class="pt2">
<form-textarea
:id="'comment'"
v-model="day.comment"
:required="false"
:label="$t('journal.journal_add_comment')"
:rows="4"
:placeholder="$t('people.life_event_create_default_description')"
/>
<div class="pv3">
<div class="flex-ns justify-between">
<div>
<a class="btn btn-secondary tc w-auto-ns w-100 mb2 pb0-ns" href="" @click.prevent="dismiss()">
{{ $t('app.cancel') }}
</a>
</div>
<div>
<button v-cy-name="'save-entry-button'" class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click="rate()">
{{ $t('app.save') }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- COME BACK LATER BOX -->
<div v-if="hasRated === 'justNow'" :key="'comeback'" class="flex items-center">
<div class="w-70 f3 pl2">
{{ $t('journal.journal_come_back') }}
</div>
</div>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
day: {
rate: '',
comment: ''
},
hasRated: 'notYet',
showSadSmileyColor: false,
showMediocreSmileyColor: false,
showHappySmileyColor: false,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.hasAlreadyRatedToday();
},
hasAlreadyRatedToday() {
axios.get('journal/hasRated')
.then(response => {
this.hasRated = response.data;
});
},
showComment(rate) {
this.day.rate = rate;
this.hasRated = 'addComment';
},
dismiss() {
this.hasRated = 'notYet';
this.day.rate = 0;
this.day.comment = '';
},
rate() {
this.hasRated = 'justNow';
axios.post('journal/day', this.day)
.then(response => {
this.showSadSmileyColor = false;
this.showMediocreSmileyColor = false;
this.showHappySmileyColor = false;
this.$emit('hasRated', response.data);
});
},
}
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div :class="[ dirltr ? 'fl' : 'fr' ]" class="journal-calendar-box relative">
<template v-if="journalEntry.show_calendar">
<div class="journal-calendar-text absolute tc">
<span class="db w-100">
{{ journalEntry.object.month_name }}
</span>
<span class="db w-100">
{{ journalEntry.object.year }}
</span>
</div>
<div class="journal-calendar-svg">
<svg width="62px" height="61px" viewBox="0 0 62 61" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<defs>
<rect id="path-1" x="0" y="8" width="62" height="53"
rx="4"
/>
<rect id="path-2" x="0" y="6" width="62" height="53"
rx="4"
/>
<rect id="path-3" x="0" y="4" width="62" height="53"
rx="4"
/>
<rect id="path-4" x="12" y="0" width="5" height="12"
rx="2.5"
/>
<rect id="path-5" x="30" y="0" width="5" height="12"
rx="2.5"
/>
<rect id="path-6" x="48" y="0" width="5" height="12"
rx="2.5"
/>
</defs>
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-87.000000, -243.000000)">
<g id="Group-6" transform="translate(87.000000, 243.000000)">
<g id="Rectangle">
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1" />
<rect stroke="#DDE2E9" stroke-width="1" x="0.5" y="8.5" width="61"
height="52" rx="4"
/>
</g>
<g id="Rectangle">
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-2" />
<rect stroke="#DDE2E9" stroke-width="1" x="0.5" y="6.5" width="61"
height="52" rx="4"
/>
</g>
<g id="Rectangle">
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-3" />
<rect stroke="#DDE2E9" stroke-width="1" x="0.5" y="4.5" width="61"
height="52" rx="4"
/>
</g>
<g id="Rectangle-2">
<use fill="#DDE2E9" fill-rule="evenodd" xlink:href="#path-4" />
<rect stroke="#DDE2E9" stroke-width="1" x="12.5" y="0.5" width="4"
height="11" rx="2"
/>
</g>
<g id="Rectangle-2">
<use fill="#DDE2E9" fill-rule="evenodd" xlink:href="#path-5" />
<rect stroke="#DDE2E9" stroke-width="1" x="30.5" y="0.5" width="4"
height="11" rx="2"
/>
</g>
<g id="Rectangle-2">
<use fill="#DDE2E9" fill-rule="evenodd" xlink:href="#path-6" />
<rect stroke="#DDE2E9" stroke-width="1" x="48.5" y="0.5" width="4"
height="11" rx="2"
/>
</g>
</g>
</g>
</g>
</svg>
</div>
</template>
<template v-else>
&nbsp;
</template>
</div>
</template>
<script>
export default {
props: {
journalEntry: {
type: Object,
default: null,
},
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
};
</script>

View File

@@ -0,0 +1,150 @@
<style scoped>
.journal-avatar-small {
height: 25px;
width: 25px;
font-size: 13px;
}
.journal-initial-small {
height: 25px;
width: 25px;
font-size: 13px;
padding-top: 2px;
}
</style>
<template>
<div>
<!-- Left columns: showing calendar -->
<journal-calendar :journal-entry="journalEntry" />
<!-- Right column: showing logs -->
<div :class="[ dirltr ? 'fl' : 'fr' ]" class="journal-calendar-content">
<div class="br3 ba b--gray-monica bg-white mb3 journal-line">
<!-- Actual log -->
<div class="flex pb3 pt3">
<!-- Day -->
<div class="flex-none w-10 tc">
<h3 class="mb0 normal fw5">
{{ activity.day }}
</h3>
<p class="mb0 black-60 f6">
{{ activity.day_name }}
</p>
</div>
<!-- Log content -->
<div class="flex-auto">
<p class="mb1">
<span class="pr2 f6 avenir">
{{ $t('journal.journal_entry_type_activity') }}: {{ activity.activity_type }}
</span>
</p>
<p class="mb1">
{{ activity.summary }}
</p>
<p v-if="showDescription" class="markdown" v-html="compiledMarkdown(activity.description)">
</p>
</div>
<!-- Comment -->
<template v-if="activity.description">
<div class="flex-none w-5 pointer" @click="toggleDescription()">
<div class="flex justify-center items-center h-100">
<svg v-tooltip.top="$t('journal.journal_show_comment')" width="16px" height="13px" viewBox="0 0 16 13" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flex-none"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"
stroke-linecap="square"
>
<g id="Desktop" transform="translate(-839.000000, -279.000000)" stroke="#979797">
<g id="Group-4" transform="translate(839.000000, 278.000000)">
<path id="Line-2" d="M0.5,1.5 L15.5,1.5" />
<path id="Line-2" d="M0.5,9.5 L15.5,9.5" />
<path id="Line-2" d="M0.5,5.5 L13.5,5.5" />
<path id="Line-2" d="M0.5,13.5 L10.5,13.5" />
</g>
</g>
</g>
</svg>
</div>
</div>
</template>
</div>
<!-- Edit/Delete/Attendees -->
<div class="flex bt b--gray-monica">
<div class="w-10">
&nbsp;
</div>
<div class="flex-none w-30 mt2 pt1 pb2">
<p class="mb0 f6 gray">
{{ $t('journal.journal_created_automatically') }}
</p>
</div>
<!-- Avatars of the attendees -->
<div class="flex-auto w-60 tr mt2 pa1 pr3 pb2">
<span class="f6 gray">
{{ $t('app.with') }}
</span>
<div v-for="attendees in activity.attendees" :key="attendees.id" class="dib pointer ml2" @click="redirect(attendees)">
<img v-tooltip.bottom="attendees.complete_name" :src="attendees.information.avatar.url" class="br3 journal-avatar-small" :alt="attendees.complete_name" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
journalEntry: {
type: Object,
default: null,
},
},
data() {
return {
showDescription: false,
activity: [],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
// not necessary, just a way to add more clarity to the code
this.activity = this.journalEntry.object;
},
toggleDescription() {
this.showDescription = !this.showDescription;
},
redirect(attendee) {
window.location.href = 'people/' + attendee.hash_id;
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
}
}
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<!-- Left columns: showing calendar -->
<journal-calendar :journal-entry="journalEntry" />
<!-- Right column: showing logs -->
<div :class="[ dirltr ? 'fl' : 'fr' ]" class="journal-calendar-content">
<div v-tooltip.top="$t('journal.journal_created_at', { date: entry.created_at })"
class="br3 ba b--gray-monica bg-white pr3 pb3 pt3 mb3 journal-line"
>
<div class="flex">
<!-- Day -->
<div class="flex-none w-10 tc">
<h3 class="mb0 normal">
{{ entry.day }}
</h3>
<p class="mb0">
{{ entry.day_name }}
</p>
</div>
<!-- Log content -->
<div class="flex-auto">
<p class="mb1">
<span class="pr2 f6 avenir">
{{ $t('journal.journal_entry_type_journal') }}
</span>
</p>
<h3 class="mb1">
{{ entry.title }}
</h3>
<span dir="auto" class="markdown" v-html="compiledMarkdown(entry.post)"></span>
<ul class="f7">
<li class="di">
<a v-cy-name="'entry-edit-button-' + entry.id" class="pointer" :href="'journal/entries/' + entry.id + '/edit'">
{{ $t('app.edit') }}
</a>
</li>
<li class="di">
<confirm v-cy-name="'entry-delete-button-' + entry.id" :message="$t('journal.delete_confirmation')" @confirm="trash()">
{{ $t('app.delete') }}
</confirm>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Confirm from '../../partials/Confirm.vue';
export default {
components: {
Confirm,
},
props: {
journalEntry: {
type: Object,
default: null,
},
},
data() {
return {
entry: [],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
// not necessary, just a way to add more clarity to the code
this.entry = this.journalEntry.object;
},
trash() {
axios.delete('journal/' + this.entry.id)
.then(response => {
this.$emit('deleteJournalEntry', this.journalEntry.id);
});
},
compiledMarkdown (text) {
return text !== undefined && text !== null ? marked(text, { sanitize: true }) : '';
}
}
};
</script>

View File

@@ -0,0 +1,185 @@
<template>
<div>
<!-- Left columns: showing calendar -->
<journal-calendar :journal-entry="journalEntry" />
<!-- Right column: showing logs -->
<div :class="[ dirltr ? 'fl' : 'fr' ]" class="journal-calendar-content">
<div class="br3 ba b--gray-monica bg-white mb3 journal-line">
<!-- Actual log -->
<div class="flex pb2 pt3">
<!-- Day -->
<div class="flex-none w-10 tc">
<h3 class="mb0 normal fw5">
{{ day.day }}
</h3>
<p class="mb0 black-60 f6">
{{ day.day_name }}
</p>
</div>
<!-- Log content -->
<div v-if="isEditingComment" class="pt2 w-100 pr-4">
<form-textarea
:id="'comment'"
v-model="day.comment"
:required="false"
:rows="4"
/>
<div class="pv3">
<div class="flex-ns justify-between">
<button v-cy-name="'save-entry-button'" class="btn btn-primary w-auto-ns w-100 mb2 pb0-ns" @click="saveComment()">
{{ $t('app.save') }}
</button>
</div>
</div>
</div>
<div class="flex-auto flex items-center">
<p v-if="!isEditingComment && day.comment" class="mb2">
{{ day.comment }}
</p>
<p v-if="!day.comment" class="mb2">
{{ $t('journal.journal_entry_rate') }}
</p>
</div>
<div class="flex-none">
<!-- sad smiley color -->
<svg v-if="day.rate === 1" width="42px" height="41px" viewBox="0 0 42 41" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mt1" :class="[ dirltr ? 'mr3' : 'ml3' ]"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-695.000000, -345.000000)">
<g id="Group" transform="translate(695.000000, 345.000000)">
<path id="Fill-87" d="M41.9467391,20.5475529 C41.9467391,31.8129081 32.5673478,40.9454502 20.9978696,40.9454502 C9.42869565,40.9454502 0.0496086957,31.8129081 0.0496086957,20.5475529 C0.0496086957,9.28219757 9.42869565,0.149951854 20.9978696,0.149951854 C32.5673478,0.149951854 41.9467391,9.28219757 41.9467391,20.5475529" fill="#FF7272" />
<path id="Fill-88" d="M36.4447391,28.5705697 C36.4447391,30.1432861 35.1342174,31.4187659 33.5184348,31.4187659 C31.9026522,31.4187659 30.5927391,30.1432861 30.5927391,28.5705697 C30.5927391,26.996668 33.5184348,21.6244798 33.5184348,21.6244798 C33.5184348,21.6244798 36.4447391,26.996668 36.4447391,28.5705697" fill="#D0021B" />
<path id="Fill-89" d="M32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 L32.3226522,15.4969018 L32.6711304,15.4939383 L32.3226522,15.4939383 C32.3226522,15.4942346 32.3220435,15.6601893 32.284913,15.8954892 C32.2575217,16.071816 32.2094348,16.2857789 32.1336522,16.4935185 C32.0767391,16.6496937 32.0043043,16.8014236 31.9184783,16.9350764 C31.7876087,17.1365927 31.6351304,17.2927678 31.4339565,17.4077507 C31.2312609,17.5212518 30.9661739,17.6095634 30.5346087,17.6116378 C30.282913,17.6113414 30.0866087,17.5793359 29.9283478,17.532513 C29.6532174,17.4486467 29.4806522,17.3265515 29.3254348,17.1608932 C29.2113043,17.0367236 29.1136087,16.8844009 29.035087,16.7154828 C28.9163913,16.4629947 28.8448696,16.1761303 28.8095652,15.9470537 C28.7733478,15.717977 28.7715217,15.5481699 28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.7636087,15.528611 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 L28.7718261,15.528611 L28.076087,15.5206096 L28.7715217,15.536316 C28.784913,14.9637726 28.3192609,14.4887274 27.7312609,14.4753918 C27.1429565,14.4623525 26.655087,14.9157643 26.6416957,15.488604 L26.6413913,15.528611 C26.6413913,15.6296655 26.6468696,15.9049723 26.7065217,16.2780739 C26.7515652,16.5581223 26.8267391,16.8935877 26.9612609,17.2533536 C27.0623043,17.5227335 27.1968261,17.8063381 27.3812609,18.0863866 C27.656087,18.5051256 28.0526522,18.9188268 28.5907391,19.2172488 C29.1276087,19.5171525 29.7916957,19.6878486 30.5346087,19.6860706 C30.9835217,19.6860706 31.4026087,19.6256157 31.7836522,19.5094474 C32.4526087,19.3082274 32.9897826,18.9303843 33.368087,18.5045329 C33.6538696,18.1850703 33.8583913,17.8421962 34.0081304,17.5129541 C34.2324348,17.0186464 34.3377391,16.5521954 34.3937391,16.1906514 C34.4491304,15.8291074 34.4527826,15.5706923 34.453087,15.4939383 L34.453087,15.4788246 C34.447913,14.9059848 33.9667391,14.4454607 33.3784348,14.4504986 C32.7901304,14.4555365 32.3174783,14.924062 32.3226522,15.4969018" fill="#36301D" />
<path id="Fill-90" d="M13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 L13.2248261,15.4969018 L13.5733043,15.4939383 L13.2248261,15.4939383 C13.2248261,15.4942346 13.2242174,15.6601893 13.187087,15.8954892 C13.1596957,16.071816 13.1116087,16.2857789 13.0358261,16.4935185 C12.978913,16.6496937 12.9064783,16.8014236 12.8206522,16.9350764 C12.6897826,17.1365927 12.5373043,17.2927678 12.3361304,17.4077507 C12.1334348,17.5212518 11.8683478,17.6095634 11.4367826,17.6116378 C11.185087,17.6113414 10.9887826,17.5793359 10.8305217,17.532513 C10.5553913,17.4486467 10.3828261,17.3265515 10.2276087,17.1608932 C10.1134783,17.0367236 10.0157826,16.8844009 9.93726087,16.7154828 C9.81856522,16.4629947 9.74704348,16.1761303 9.71173913,15.9470537 C9.67552174,15.717977 9.67369565,15.5481699 9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L9.66578261,15.528611 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 L9.674,15.528611 L8.97826087,15.5206096 L9.67369565,15.536316 C9.68708696,14.9637726 9.22143478,14.4887274 8.63343478,14.4753918 C8.04513043,14.4623525 7.55726087,14.9157643 7.54386957,15.488604 L7.54356522,15.528611 C7.54356522,15.6296655 7.54904348,15.9049723 7.60869565,16.2780739 C7.65373913,16.5581223 7.72891304,16.8935877 7.86343478,17.2533536 C7.96447826,17.5227335 8.099,17.8063381 8.28343478,18.0863866 C8.55826087,18.5051256 8.95482609,18.9188268 9.49291304,19.2172488 C10.0297826,19.5171525 10.6938696,19.6878486 11.4367826,19.6860706 C11.8856957,19.6860706 12.3047826,19.6256157 12.6858261,19.5094474 C13.3547826,19.3082274 13.8919565,18.9303843 14.2702609,18.5045329 C14.5560435,18.1850703 14.7605652,17.8421962 14.9103043,17.5129541 C15.1346087,17.0186464 15.239913,16.5521954 15.295913,16.1906514 C15.3513043,15.8291074 15.3549565,15.5706923 15.3552609,15.4939383 L15.3552609,15.4788246 C15.350087,14.9059848 14.868913,14.4454607 14.2806087,14.4504986 C13.6923043,14.4555365 13.2196522,14.924062 13.2248261,15.4969018" fill="#36301D" />
<path id="Fill-91" d="M14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 L14.5152609,28.1364206 L14.4954783,28.13079 L14.5152609,28.1367169 C14.5359565,28.0605556 14.9410435,26.8330841 15.946913,25.6951096 C16.4493913,25.1240479 17.0946087,24.5740268 17.9184783,24.1653636 C18.7438696,23.757293 19.7503478,23.4816898 21.0316522,23.4802081 C22.3558696,23.4816898 23.3818261,23.7729994 24.2145217,24.1988509 C24.8381304,24.5192025 25.353087,24.9201608 25.7794783,25.3477903 C26.418913,25.9884936 26.8541304,26.6917263 27.1222609,27.2287081 C27.2564783,27.4969026 27.349,27.7227194 27.4062174,27.8756348 C27.4348261,27.9520924 27.4546087,28.0101765 27.4661739,28.0463309 L27.4780435,28.0836707 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 L27.4795652,28.089005 L27.9382174,27.9615755 L27.4783478,28.0848561 C27.6347826,28.6369516 28.2215652,28.9614521 28.7885652,28.8091295 C29.3555652,28.6568069 29.6885217,28.0857452 29.5323913,27.5333533 C29.505913,27.4509687 29.0932174,25.9884936 27.8265217,24.4703052 C27.1925652,23.7134336 26.3367391,22.9435227 25.2082174,22.3638669 C24.0812174,21.7836184 22.6845652,21.4042936 21.0316522,21.4057753 C19.3775217,21.4042936 17.9766087,21.7868783 16.8444348,22.3709792 C15.9943913,22.8077955 15.2953043,23.3533713 14.7292174,23.9188024 C13.8797826,24.7681345 13.3249565,25.6610296 12.9749565,26.3482595 C12.6255652,27.0357858 12.4806957,27.5200177 12.467,27.5665443 C12.305087,28.117158 12.6328696,28.6911832 13.1983478,28.8488401 C13.7641304,29.0062006 14.3536522,28.6870343 14.5152609,28.1364206" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- mediocre day color -->
<svg v-else-if="day.rate === 2" width="42px" height="41px" viewBox="0 0 42 41" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mt1" :class="[ dirltr ? 'mr3' : 'ml3' ]"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-754.000000, -165.000000)">
<g id="Group-2" transform="translate(754.000000, 165.000000)">
<path id="Fill-96" d="M41.8643309,20.5475529 C41.8643309,31.8129081 32.5521151,40.9454502 21.0661727,40.9454502 C9.57992806,40.9454502 0.268618705,31.8129081 0.268618705,20.5475529 C0.268618705,9.28219757 9.57992806,0.149951854 21.0661727,0.149951854 C32.5521151,0.149951854 41.8643309,9.28219757 41.8643309,20.5475529" fill="#FFEA7C" />
<path id="Fill-97" d="M11.5989496,21.8043628 L11.5989496,21.8043628 C10.7680144,21.8043628 10.0881583,21.1375808 10.0881583,20.3226251 L10.0881583,16.7664546 C10.0881583,15.9514989 10.7680144,15.2847169 11.5989496,15.2847169 C12.4298849,15.2847169 13.109741,15.9514989 13.109741,16.7664546 L13.109741,20.3226251 C13.109741,21.1375808 12.4298849,21.8043628 11.5989496,21.8043628" fill="#36301D" />
<path id="Fill-98" d="M30.534,21.8043628 L30.534,21.8043628 C29.7030647,21.8043628 29.0232086,21.1375808 29.0232086,20.3226251 L29.0232086,16.7664546 C29.0232086,15.9514989 29.7030647,15.2847169 30.534,15.2847169 C31.3649353,15.2847169 32.0447914,15.9514989 32.0447914,16.7664546 L32.0447914,20.3226251 C32.0447914,21.1375808 31.3649353,21.8043628 30.534,21.8043628" fill="#36301D" />
<path id="Fill-99" d="M9.15690647,27.2639734 L32.9760432,27.2639734 C33.4767194,27.2639734 33.882518,26.8659787 33.882518,26.3749308 C33.882518,25.883883 33.4767194,25.4858882 32.9760432,25.4858882 L9.15690647,25.4858882 C8.65623022,25.4858882 8.25043165,25.883883 8.25043165,26.3749308 C8.25043165,26.8659787 8.65623022,27.2639734 9.15690647,27.2639734" fill="#36301D" />
</g>
</g>
</g>
</svg>
<!-- happy day color -->
<svg v-else-if="day.rate === 3" width="42px" height="42px" viewBox="0 0 42 42" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mt1" :class="[ dirltr ? 'mr3' : 'ml3' ]"
>
<defs />
<g id="App" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop" transform="translate(-814.000000, -345.000000)">
<g id="Group-3" transform="translate(814.000000, 345.000000)">
<path id="Fill-11" d="M42,20.5961538 C42,31.9710023 32.5972745,41.1923077 20.9996949,41.1923077 C9.40181023,41.1923077 0,31.9710023 0,20.5961538 C0,9.22130542 9.40181023,0 20.9996949,0 C32.5972745,0 42,9.22130542 42,20.5961538" fill="#C4F1AF" />
<path id="Fill-12" d="M11.3076923,21 L11.3076923,21 C10.4192308,21 9.69230769,20.3391608 9.69230769,19.5314685 L9.69230769,16.006993 C9.69230769,15.1993007 10.4192308,14.5384615 11.3076923,14.5384615 C12.1961538,14.5384615 12.9230769,15.1993007 12.9230769,16.006993 L12.9230769,19.5314685 C12.9230769,20.3391608 12.1961538,21 11.3076923,21" fill="#36301D" />
<path id="Fill-13" d="M30.6923077,21 L30.6923077,21 C29.8038462,21 29.0769231,20.3391608 29.0769231,19.5314685 L29.0769231,16.006993 C29.0769231,15.1993007 29.8038462,14.5384615 30.6923077,14.5384615 C31.5807692,14.5384615 32.3076923,15.1993007 32.3076923,16.006993 L32.3076923,19.5314685 C32.3076923,20.3391608 31.5807692,21 30.6923077,21" fill="#36301D" />
<path id="Fill-14" d="M27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0720842,24.0517885 L27.6146482,24.3231607 L27.078106,24.0385354 L27.0756973,24.0432687 C27.0404698,24.1114273 26.6683226,24.8097374 25.6979098,25.4916389 C24.7250883,26.1716472 23.1440652,26.864593 20.5631224,26.868064 C19.1874426,26.8677485 18.0993036,26.6692681 17.239993,26.3852739 C15.9516293,25.9577049 15.1687955,25.3417531 14.6996974,24.8539142 C14.4660516,24.6096793 14.3127969,24.3966836 14.222771,24.2556332 C14.1776075,24.1852658 14.1484018,24.1325691 14.1330462,24.1035385 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L14.1200993,24.0782946 L14.0261593,24.1278358 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 L14.1210026,24.0801879 L13.4688418,24.4099367 L14.1264222,24.0921788 C13.8828405,23.5383902 13.2565734,23.296364 12.7281606,23.5516432 C12.1997478,23.8069224 11.9688119,24.4629489 12.2123936,25.0170531 C12.2659876,25.1388551 12.7408064,26.1596563 14.0342887,27.154267 C14.6795244,27.6503102 15.5267914,28.1353091 16.6058977,28.491564 C17.6847028,28.84845 18.9935407,29.0772231 20.5631224,29.0769075 C23.7028881,29.0813252 25.8180447,28.1431978 27.1220651,27.1429072 C28.4293976,26.1448255 28.919873,25.1224465 28.9737681,25.0038001 C29.224576,24.4531669 29.0023717,23.7939849 28.4769698,23.5311325 C27.9518689,23.2682801 27.3228921,23.5014709 27.0720842,24.0517885" fill="#36301D" />
<path id="Fill-15" d="M7.26939206,25.8461538 C5.48457391,25.8461538 4.03846154,24.5805233 4.03846154,23.0190896 C4.03846154,21.457656 5.48457391,20.1923077 7.26939206,20.1923077 C9.05324247,20.1923077 10.5,21.457656 10.5,23.0190896 C10.5,24.5805233 9.05324247,25.8461538 7.26939206,25.8461538" fill="#EB9260" />
<path id="Fill-16" d="M37.1540074,25.8461538 C35.3691893,25.8461538 33.9230769,24.5805233 33.9230769,23.0190896 C33.9230769,21.457656 35.3691893,20.1923077 37.1540074,20.1923077 C38.9381804,20.1923077 40.3846154,21.457656 40.3846154,23.0190896 C40.3846154,24.5805233 38.9381804,25.8461538 37.1540074,25.8461538" fill="#EB9260" />
</g>
</g>
</g>
</svg>
</div>
</div>
<!-- Edit/Delete -->
<div class="flex bt b--gray-monica">
<div class="w-10">
&nbsp;
</div>
<div class="flex-none w-90 mt2 pt0 pr3 pb2">
<ul class="f7">
<li class="di">
{{ $t('journal.journal_entry_rate') }}
</li>
<li class="di">
<a v-cy-name="'entry-edit-button-' + journalEntry.id" class="pointer" :href="'journal/day/' + journalEntry.id + '/edit'" @click.prevent="editingComment">
{{ $t('app.edit') }}
</a>
</li>
<li class="di">
<a v-cy-name="'entry-delete-button-' + journalEntry.id" class="pointer" href="" @click.prevent="destroy()">
{{ $t('app.delete') }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
journalEntry: {
type: Object,
default: null,
},
},
data() {
return {
day: [],
isEditingComment: false,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.day = this.journalEntry.object;
},
editingComment() {
this.isEditingComment = !this.isEditingComment;
},
saveComment() {
axios.put('journal/day/' + this.day.id + '/update', {
comment: this.day.comment,
})
.then(response => {
this.editingComment();
});
},
destroy() {
axios.delete('journal/day/' + this.day.id)
.then(response => {
this.$emit('deleteJournalEntry', this.journalEntry.id);
});
},
}
};
</script>

View File

@@ -0,0 +1,58 @@
<style scoped>
.avatar-padding {
padding-top: 19%
}
</style>
<template>
<span>
<img v-if="check"
v-tooltip.bottom="contact.complete_name"
:class="['br4 h3 w3 dib tc', imgclass]"
:alt="contact.initials"
:src="avatar_url"
@error="check=false"
/>
<span v-else
v-tooltip.bottom="contact.complete_name"
:class="['br4 h3 w3 dib tc', 'white f3 avatar-padding', imgclass]"
:style="'background-color: '+ default_avatar_color"
>
{{ contact.initials }}
</span>
</span>
</template>
<script>
export default {
props: {
contact: {
type: Object,
default: null,
},
imgclass: {
type: String,
default: '',
},
},
data () {
return {
check: true,
};
},
computed: {
avatar_url() {
if (_.isObject(this.contact.information)) {
return this.contact.information.avatar.url;
}
return this.contact.avatar_url;
},
default_avatar_color() {
if (_.isObject(this.contact.information)) {
return this.contact.information.avatar.default_avatar_color;
}
return this.contact.default_avatar_color;
}
}
};
</script>

View File

@@ -0,0 +1,38 @@
<template>
<div class="avatars">
<a v-if="clickable === true" :href="'people/' + id">
<avatarimg :contact="contact" :imgclass="imgclass" />
</a>
<avatarimg v-else :contact="contact" :imgclass="imgclass" />
</div>
</template>
<script>
import Avatarimg from './Avatar.img.vue';
export default {
components: {
Avatarimg
},
props: {
contact: {
type: Object,
default: null,
},
clickable: {
type: Boolean,
default: true,
},
imgclass: {
type: String,
default: '',
},
},
computed: {
id() {
return this.contact.hash_id ? this.contact.hash_id : this.contact.id;
},
}
};
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div class="di">
<a :class="lclass" :title="title" href="" @click.prevent="open">
<slot>
</slot>
</a>
<sweet-modal ref="modal" tabindex="-1" role="dialog">
<div>
{{ message }}
</div>
<!-- Modal Actions -->
<div slot="button" class="flex-ns justify-between">
<a class="btn mt2" href="" @click.prevent="close">
{{ $t('app.cancel') }}
</a>
<button v-cy-name="'confirm-' + name" class="btn btn-primary w-auto-ns w100 mt2 pb0-ns" @click="confirm($event)">
{{ $t('app.confirm') }}
</button>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal,
},
props: {
name: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
linkClass: {
type: [String, Array],
default: '',
},
},
computed: {
lclass() {
return this.linkClass === '' ? 'pointer' : this.linkClass;
}
},
methods: {
open() {
this.$refs.modal.open();
},
close() {
this.$refs.modal.close();
},
confirm(event) {
this.close();
this.$emit('confirm', event);
}
}
};
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div v-if="apierror || errors.length > 0" class="alert alert-danger">
<p>{{ $t('app.error_title') }}</p>
<template v-if="apierror">
<ul v-if="apimessage">
<li v-for="error in errors[0].message" :key="error.id">
▪️ {{ error }}
</li>
</ul>
<p v-else>
{{ errors[0].message }}
</p>
</template>
<template v-else>
<p v-if="errors[0] !== 'The given data was invalid.'">
{{ errors[0] }}
</p>
<template v-if="display(errors[1])">
<ul v-for="errorsList in errors[1]" :key="errorsList.id">
<li v-for="error in errorsList" :key="error.id">
▪️ {{ error }}
</li>
</ul>
</template>
</template>
</div>
</template>
<script>
export default {
props: {
errors: {
type: [Array, Object],
default: () => [],
},
},
computed: {
apierror() {
return _.isObject(this.errors[0]) && this.errors[0].error_code !== undefined;
},
apimessage() {
return _.isArray(this.errors[0].message);
}
},
methods: {
display($val) {
return _.isObject($val);
},
}
};
</script>

View File

@@ -0,0 +1,255 @@
<style scoped>
</style>
<template>
<div>
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb3 mb0-ns">
<form-radio
v-model.lazy="selectedOption"
:name="'birthdate'"
:value="'unknown'"
:dclass="'flex mb3'"
:iclass="[ dirltr ? 'mr2' : 'ml2' ]"
@change="event => { _saveOption(); }"
>
<template slot="label">
{{ $t('people.information_edit_unknown') }}
</template>
</form-radio>
<form-radio
v-model.lazy="selectedOption"
:name="'birthdate'"
:value="'approximate'"
:dclass="'flex mb3'"
:iclass="[ dirltr ? 'mr2' : 'ml2' ]"
@change="event => { if (selectedOptionSave !== 'approximate') {_focusAge();} _saveOption(); }"
>
<template slot="label">
{{ $t('people.information_edit_probably') }}
</template>
<div v-if="selectedOption === 'approximate'" slot="extra">
<form-input
:id="'age'"
ref="age"
v-model="selectedAge"
:input-type="'number'"
:width="50"
:required="true"
:validator="$v.selectedAge"
/>
</div>
</form-radio>
<form-radio
v-model.lazy="selectedOption"
:name="'birthdate'"
:value="'almost'"
:dclass="'flex mb3'"
:iclass="[ dirltr ? 'mr2' : 'ml2' ]"
@change="event => { if (selectedOptionSave !== 'almost') {_focusMonth();} _saveOption(); }"
>
<template slot="label">
{{ $t('people.information_edit_not_year') }}
</template>
<div v-if="selectedOption === 'almost'" slot="extra" class="mt2">
<div class="flex">
<form-select
:id="'month'"
ref="month"
v-model="selectedMonth"
:options="months"
:title="''"
:class="[ dirltr ? 'mr3' : '' ]"
/>
<form-select
:id="'day'"
v-model="selectedDay"
:options="days"
:title="''"
:class="[ dirltr ? '' : 'mr3' ]"
/>
</div>
</div>
</form-radio>
<form-radio
v-model.lazy="selectedOption"
:name="'birthdate'"
:value="'exact'"
:dclass="'flex mb3'"
:iclass="[ dirltr ? 'mr2' : 'ml2' ]"
@change="event => { if (selectedOptionSave !== 'exact') {_focusBirthday();} _saveOption(); }"
>
<template slot="label">
{{ $t('people.information_edit_exact') }}
</template>
<div v-if="selectedOption === 'exact'" slot="extra" class="mt2">
<form-date
:id="'birthdayDate'"
ref="birthday"
v-model="selectedDate"
:show-calendar-on-focus="true"
:locale="locale"
:label="$t('people.information_edit_birthdate_label')"
:class="[ dirltr ? 'fl' : 'fr' ]"
:validator="$v.selectedDate"
/>
</div>
</form-radio>
</div>
</div>
<div v-if="selectedOption === 'exact' || selectedOption === 'almost'" class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb2 mb0-ns">
<form-checkbox
v-model.lazy="hasBirthdayReminder"
:name="'addReminder'"
:value="'addReminder'"
:dclass="[ 'flex', dirltr ? 'mr2' : 'ml2' ]"
>
<template slot="label">
{{ $t('people.people_add_reminder_for_birthday') }}
</template>
</form-checkbox>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { validationMixin } from 'vuelidate';
import { required, numeric, helpers } from 'vuelidate/lib/validators';
const before = (param) =>
helpers.withParams(
{ type: 'before', date: param },
(value) => !helpers.req(value) || moment(value).isBefore(param)
);
export default {
mixins: [validationMixin],
props: {
value: {
type: String,
default: '',
},
days: {
type: Array,
default: () => [],
},
months: {
type: Array,
default: () => [],
},
day: {
type: Number,
default: 0,
},
month: {
type: Number,
default: 0,
},
birthdate: {
type: String,
default: '',
},
age: {
type: Number,
default: 0,
},
reminder: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedDate: null,
selectedOption: null,
selectedOptionSave: null,
selectedAge: 0,
selectedMonth: 0,
selectedDay: 0,
hasBirthdayReminder: false
};
},
validations() {
switch (this.selectedOption) {
case 'approximate':
return {
selectedAge: {
required,
numeric,
}
};
case 'exact':
return {
selectedDate: {
required,
before: before(moment())
}
};
}
return null;
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
locale() {
return this.$root.locale;
}
},
watch: {
birthdate(val) {
this.selectedDate = val;
},
value(val) {
this.selectedOption = val;
},
age(val) {
this.selectedAge = val;
},
},
mounted() {
this.selectedDate = this.birthdate;
this.selectedOption = this.value !== '' ? this.value : 'unknown';
this.selectedOptionSave = this.selectedOption;
this.selectedAge = this.age;
this.selectedMonth = this.month;
this.selectedDay = this.day;
this.hasBirthdayReminder = this.reminder;
},
methods: {
_focusAge() {
setTimeout(() => {
this.$refs.age.focus();
}, 100);
},
_focusMonth() {
setTimeout(() => {
this.$refs.month.focus();
}, 100);
},
_focusBirthday() {
setTimeout(() => {
this.$refs.birthday.focus();
}, 100);
},
_saveOption() {
this.selectedOptionSave = this.selectedOption;
},
}
};
</script>

View File

@@ -0,0 +1,133 @@
<style scoped>
</style>
<template>
<div class="pa4-ns ph3 pv2 bb b--gray-monica">
<div class="mb3 mb0-ns">
<form-checkbox
v-model.lazy="deceased"
:name="'is_deceased'"
:value="true"
:dclass="'flex mb2'"
>
<template slot="label">
{{ $t('people.deceased_mark_person_deceased') }}
</template>
</form-checkbox>
<div v-show="deceased" :class="[ dirltr ? 'ml4' : 'mr4' ]">
<form-checkbox
v-model.lazy="dateKnown"
:name="'is_deceased_date_known'"
:value="true"
:dclass="'flex mb1'"
@change="_focusDate()"
>
<template slot="label">
{{ $t('people.deceased_know_date') }}
</template>
</form-checkbox>
<div v-show="dateKnown" :class="[ dirltr ? 'ml4' : 'mr4' ]">
<form-date
:id="'deceased_date'"
ref="deaceasedday"
v-model="selectedDate"
:label="$t('people.deceased_date_label')"
:show-calendar-on-focus="true"
:locale="locale"
:validator="$v.selectedDate"
/>
<div v-show="selectedDate !== ''" class="mt2">
<form-checkbox
:name="'add_reminder_deceased'"
:value="true"
:model="reminder"
>
{{ $t('people.deceased_add_reminder') }}
</form-checkbox>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { validationMixin } from 'vuelidate';
import { required, numeric, helpers } from 'vuelidate/lib/validators';
const before = (param) =>
helpers.withParams(
{ type: 'before', date: param },
(value) => !helpers.req(value) || moment(value).isBefore(param)
);
export default {
mixins: [validationMixin],
props: {
value: {
type: Boolean,
default: false,
},
date: {
type: String,
default: '',
},
reminder: {
type: Boolean,
default: false,
},
},
data() {
return {
deceased: false,
dateKnown: false,
selectedDate: null,
};
},
validations: {
selectedDate: {
required,
before: before(moment())
}
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
locale() {
return this.$root.locale;
}
},
watch: {
value(val) {
this.deceased = val;
},
date(val) {
this.selectedDate = val;
},
},
mounted() {
this.deceased = this.value;
this.dateKnown = this.date !== '';
this.selectedDate = this.date;
},
methods: {
_focusDate() {
setTimeout(() => {
this.$refs.deaceasedday.focus();
}, 100);
},
}
};
</script>

View File

@@ -0,0 +1,23 @@
<script>
const input = require('./PInput').default;
const checkbox = {
name: 'checkbox',
input_type: 'checkbox',
input_iclass: 'p-curve p-thick',
components: input.components,
model: input.model,
props: input.props,
data: input.data,
computed: input.computed,
watch: input.watch,
mounted: input.mounted,
methods: input.methods,
render: input.render,
};
export default checkbox;
</script>

View File

@@ -0,0 +1,206 @@
<template>
<div :class="{ 'form-group-error': validator && validator.$error }">
<datepicker
ref="select"
:ref-name="'select'"
:value="selectedDate"
:format="displayValue"
:parse-typed-date="formatTypedValue"
:language="locale"
:monday-first="mondayFirst"
:input-class="inputClass"
:typeable="true"
:clear-button="true"
:show-calendar-on-focus="showCalendarOnFocus"
@input="onInput($event)"
@selected="onSelected($event)"
@clearDate="onSelected('')"
/>
<input :name="id" type="hidden" :value="exchange" />
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
<small v-if="validator && (validator.$error && validator.before !== undefined && !validator.before)" class="error">
{{ beforeMessage }}
</small>
</div>
</template>
<script>
import Datepicker from '@hokify/vuejs-datepicker';
import moment from 'moment';
export default {
components: {
Datepicker
},
model: {
prop: 'value',
event: 'input'
},
props: {
id: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
defaultDate: {
type: String,
default: '',
},
locale: {
type: String,
default: '',
},
showCalendarOnFocus: {
type: Boolean,
default: false,
},
validator: {
type: Object,
default: null,
},
},
data() {
return {
/**
* Value of the date in exchange format
*/
exchange: '',
selectedDate: '',
mondayFirst: false
};
},
computed: {
/**
* Exchange format with controller (moment format type).
*/
exchangeFormat() {
return 'YYYY-MM-DD';
},
/**
* Display format (moment format type).
*/
displayFormat() {
return 'L';
},
inputClass() {
var c = ['br2 f5 ba b--black-40 pa2 outline-0'];
if (this.validator) {
c.push({ 'error': this.validator.$error });
}
return c;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.label });
},
beforeMessage() {
return this.$t('validation.vue.max.numeric', {
field: this.label,
max: this.displayValue(this.validator.$params.before.date)
});
},
},
watch: {
value: function (newValue) {
this.updateExchange(newValue);
}
},
mounted() {
this.updateExchange(this.value === '' ? this.defaultDate : this.value);
this.mondayFirst = moment.localeData().firstDayOfWeek() === 1;
},
methods: {
/**
* Format date for display it.
* @param date string in locale format
* @return string date in display format
*/
displayValue(date) {
return date !== '' && date !== null ? moment(date).format(this.displayFormat) : '';
},
/**
* Format date for save it.
* @param date string in locale format
* @return string date in exchange format
*/
exchangeValue(date) {
return date !== '' && date !== null ? moment(date).format(this.exchangeFormat) : '';
},
/**
* Update the value of hidden input.
* Store it in exchange format value.
*/
update(date) {
if (date === '' || date === null) {
this.exchange = '';
} else {
var mdate = moment(date);
if (! mdate.isValid()) {
mdate = moment();
}
this.exchange = mdate.format(this.exchangeFormat);
}
},
updateExchange(date) {
this.exchange = date;
if (this.exchange !== '') {
var mdate = moment(this.exchange, this.exchangeFormat);
if (! mdate.isValid()) {
mdate = moment();
}
this.selectedDate = mdate.toDate();
}
this.update(this.selectedDate);
},
/**
* Format the typed value with the locale specification.
* @param date string in locale format
* @return date value
*/
formatTypedValue(date) {
return date !== '' && date !== null ? moment(date, this.displayFormat).toDate() : '';
},
focus() {
this.$refs.select.$children[0].$refs.select.focus();
},
onInput(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('input', this.exchangeValue(event));
},
onSelected(event) {
this.update(event);
this.onInput(event);
}
}
};
</script>

View File

@@ -0,0 +1,181 @@
<style scoped>
.input {
transition: all;
transition-duration: 0.2s;
border: 1px solid #c4cdd5;
}
.input:focus {
border: 1px solid #5c6ac4;
}
</style>
<template>
<div :class="{ 'form-group-error': validator && validator.$error }">
<label
v-if="title"
:for="realid"
class="mb2"
:class="{ b: required, error: validator && validator.$error }"
>
{{ title }}
</label>
<input
:id="realid"
ref="input"
:type="inputType"
autofocus
:required="required"
:name="id"
:placeholder="placeholder"
:class="inputClass"
:style="inputStyle"
:value="value"
:maxlength="maxlength"
:step="step"
@input="onInput($event)"
@blur="onBlur($event)"
@change="onChange($event)"
@keyup.enter="onSubmit($event)"
/>
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
<small v-if="validator && (validator.$error && validator.maxLength !== undefined && !validator.maxLength)" class="error">
{{ maxLengthMessage }}
</small>
<small v-if="validator && (validator.$error && validator.url !== undefined && !validator.url)" class="error">
{{ urlMessage }}
</small>
</div>
</template>
<script>
export default {
props: {
value: {
type: [String, Number],
default: '',
},
title: {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
id: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
required: {
type: Boolean,
default: true,
},
inputType: {
type: String,
default: '',
},
step: {
type: String,
default: '',
},
width: {
type: Number,
default: -1,
},
iclass: {
type: [String, Array],
default: ''
},
maxlength: {
type: Number,
default: null,
},
validator: {
type: Object,
default: null,
},
},
computed: {
realid() {
return this.id + this._uid;
},
inputClass() {
var c = [this.iclass !== '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'];
if (this.validator) {
c.push({ error: this.validator.$error });
}
c.push('input');
return c;
},
inputStyle() {
return this.width >= 0 ? 'width:' + this.width + 'px' : '';
},
field() {
return this.label && this.label.length > 0 ? this.label : this.title;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.field });
},
urlMessage() {
return this.$t('validation.vue.url', { field: this.field });
},
maxLengthMessage() {
var type = 'string';
switch (this.inputType) {
case 'number':
type = 'numeric';
break;
}
return this.$t(`validation.vue.max.${type}`, {
field: this.field,
max: this.validator ? this.validator.$params.maxLength.max : '',
});
},
},
methods: {
focus() {
this.$refs.input.focus();
},
onInput(event) {
if (this.validator && event.data !== undefined) {
this.validator.$reset();
}
this.$emit('input', event.target.value);
},
onSubmit(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('submit', event.target.value);
},
onBlur(event) {
if (this.validator && event.target.value !== '') {
this.validator.$touch();
}
this.$emit('blur', event.target.value);
},
onChange(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('change', event.target.value);
},
},
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div :class="dclass">
<div>
<p-input
ref="input"
v-model.lazy="prop"
:name="name"
:type="_type"
:class="inputClass"
:color="inputColor"
:value="value"
:disabled="disabled"
:required="required"
@change="$emit('change', $event)"
>
<slot></slot>
<slot slot="extra" name="inputextra"></slot>
</p-input>
</div>
<div class="pointer" @click="select()">
<label v-if="hasSlot('label')" class="pointer">
<slot name="label"></slot>
</label>
<slot name="extra"></slot>
</div>
</div>
</template>
<script>
import PInput from 'pretty-checkbox-vue/input';
export default {
components: {
PInput
},
model: {
prop: 'modelValue',
event: 'change'
},
props: {
name: {
type: String,
default: '',
},
value: {
type: [String, Boolean],
default: '',
},
modelValue: {
type: [String, Boolean],
default: '',
},
iclass: {
type: [String, Array],
default: ''
},
fullClass: {
type: [String, Array],
default: ''
},
dclass: {
type: [String, Array],
default: ''
},
color: {
type: [String, Array],
default: ''
},
disabled: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
}
},
data() {
return {
prop: null
};
},
computed: {
_type() {
if (this.$options.input_type) {
return this.$options.input_type;
}
return 'input';
},
inputClass() {
return this.fullClass !== '' ? this.fullClass : [this.iclass, 'p-default', this.$options.input_iclass];
},
inputColor() {
return this.color !== '' ? this.color : 'primary-o';
},
},
watch: {
modelValue(val) {
this.prop = val;
},
},
mounted() {
this.prop = this.modelValue;
},
methods: {
select() {
if (this.disabled) {
return;
}
switch (this._type)
{
case 'checkbox':
this.$refs.input.$refs.input.checked = ! this.$refs.input.$refs.input.checked;
this.$emit('change', this.$refs.input.$refs.input.checked);
break;
case 'radio':
this.$refs.input.$refs.input.checked = true;
this.$emit('change', this.value);
break;
case 'input':
//this.$refs.input.$refs.input.focus();
}
},
hasSlot (name = 'default') {
return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}
}
};
</script>

View File

@@ -0,0 +1,23 @@
<script>
const input = require('./PInput').default;
const radio = {
name: 'radio',
input_type: 'radio',
input_iclass: 'p-round p-thick',
components: input.components,
model: input.model,
props: input.props,
data: input.data,
computed: input.computed,
watch: input.watch,
mounted: input.mounted,
methods: input.methods,
render: input.render,
};
export default radio;
</script>

View File

@@ -0,0 +1,164 @@
<style scoped>
.select {
height: 34px;
transition: all;
transition-duration: 0.2s;
border: 1px solid #c4cdd5;
}
.select:focus {
border: 1px solid #5c6ac4;
}
</style>
<template>
<div :class="{ 'form-group-error': validator && validator.$error }">
<label
v-if="title"
:for="realid"
class="mb2"
:class="{ b: required, error: validator && validator.$error }"
>
{{ title }}
</label>
<select
:id="realid"
ref="select"
:value="selectedOption"
:name="id"
:required="required"
:class="selectClass"
@input="onInput"
>
<template v-if="Array.isArray(options)">
<option
v-for="option in filterExclude(options)"
:key="option.id"
:value="option.id"
>
{{ option.name }}
</option>
</template>
<template v-else>
<optgroup
v-for="(optgroup,index) in options"
:key="index"
:label="optgroup.name"
>
<option
v-for="option in filterExclude(optgroup.options)"
:key="option.id"
:value="option.id"
>
{{ option.name }}
</option>
</optgroup>
</template>
</select>
<small v-if="validator && (validator.$error && validator.required !== undefined && !validator.required)" class="error">
{{ requiredMessage }}
</small>
</div>
</template>
<script>
export default {
props: {
value: {
type: [String, Number],
default: '',
},
options: {
type: [Array, Object],
default: () => [],
},
title: {
type: String,
default: '',
},
label: {
type: String,
default: null,
},
id: {
type: String,
default: '',
},
excludedId: {
type: Number,
default: -1,
},
required: {
type: Boolean,
default: true,
},
iclass: {
type: String,
default: '',
},
validator: {
type: Object,
default: null,
}
},
data() {
return {
selectedOption: null,
};
},
computed: {
realid() {
return this.id + this._uid;
},
selectClass() {
var c = [this.iclass !== '' ? this.iclass : 'br2 f5 w-100 ba b--black-40 pa2 outline-0'];
if (this.validator) {
c.push({ error: this.validator.$error });
}
c.push('select');
return c;
},
field() {
return this.label && this.label.length > 0 ? this.label : this.title;
},
requiredMessage() {
return this.$t('validation.vue.required', { field: this.field });
},
},
watch: {
value: function (newValue) {
this.selectedOption = newValue;
}
},
mounted() {
this.selectedOption = this.value;
},
methods: {
/**
* Filter options
*/
filterExclude: function (options) {
var me = this;
return options.filter(function (option) {
return option.id !== me.excludedId;
});
},
focus() {
this.$refs.select.focus();
},
onInput(event) {
if (this.validator) {
this.validator.$touch();
}
this.$emit('input', event.target.value);
},
},
};
</script>

View File

@@ -0,0 +1,96 @@
<style scoped>
textarea {
transition: all;
transition-duration: 0.2s;
border: 1px solid #c4cdd5;
}
textarea:focus {
border: 1px solid #5c6ac4;
}
</style>
<template>
<div>
<label
v-if="label"
:for="realid"
class="mb2"
:class="{ b: required }"
>
{{ label }}
</label>
<textarea
:id="realid"
v-model="buffer"
autofocus
:required="required"
:name="id"
:placeholder="placeholder"
:rows="rows"
class="br2 f5 w-100 ba b--black-40 pa2 outline-0"
:style="textareaStyle"
@input="$emit('input', buffer)"
></textarea>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
required: {
type: Boolean,
default: true,
},
width: {
type: Number,
default: -1,
},
rows: {
type: Number,
default: 0,
}
},
data() {
return {
buffer: this.value
};
},
computed: {
realid() {
return this.id + this._uid;
},
textareaStyle() {
return this.width >= 0 ? 'width:' + this.width + 'px' : '';
}
},
watch: {
value: function (newValue) {
this.buffer = newValue;
}
},
mounted() {
this.buffer = this.value;
},
};
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div>
<label
v-if="title"
:for="realId"
class="mb2"
:class="{ b: required }"
>
{{ title }}
</label>
<toggle-button
:id="realId"
:name="id"
:class="inputClass"
:sync="true"
:labels="labels"
:disabled="disabled"
:value="selectedOption"
@input="$emit('input', $event)"
@change="$emit('change', $event)"
/>
</div>
</template>
<script>
import { ToggleButton } from 'vue-js-toggle-button';
export default {
components: {
ToggleButton
},
props: {
value: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
labels: {
type: [Boolean, Object],
default: false,
},
id: {
type: String,
default: '',
},
required: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
iclass: {
type: String,
default: ''
},
},
data() {
return {
selectedOption: null,
};
},
computed: {
realId() {
return this.id + this._uid;
},
inputClass() {
return this.iclass !== '' ? this.iclass : '';
},
},
watch: {
value: function (newValue) {
this.selectedOption = newValue;
}
},
mounted() {
this.selectedOption = this.value;
},
};
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div>
<h3 class="mb3">
{{ $t('settings.api_authorized_clients') }}
</h3>
<p>{{ $t('settings.api_authorized_clients_desc') }}</p>
<!-- Authorized Clients -->
<p v-if="tokens.length === 0" class="mb0">
{{ $t('settings.api_authorized_clients_none') }}
</p>
<div v-else class="dt dt--fixed w-100 collapse br--top br--bottom">
<em>{{ $t('settings.api_authorized_clients_title') }}</em>
<div class="dt-row">
<div class="dtc w-20">
<div class="pa2 b">
{{ $t('settings.api_authorized_clients_name') }}
</div>
</div>
<div class="dtc w-20">
<div class="pa2 b">
{{ $t('settings.api_authorized_clients_scopes') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="token in tokens" :key="token.id" class="dt-row bb b--light-gray">
<!-- Client Name -->
<div class="dtc">
<div class="pa2">
{{ token.client.name }}
</div>
</div>
<!-- Scopes -->
<div class="dtc">
<div class="pa2">
<span v-if="token.scopes.length > 0">
{{ token.scopes.join(', ') }}
</span>
</div>
</div>
<!-- Revoke Button -->
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<span class="pointer" @click="revoke(token)">{{ $t('app.revoke') }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
tokens: []
};
},
mounted() {
this.getTokens();
},
methods: {
/**
* Get all of the authorized tokens for the user.
*/
getTokens() {
axios.get('oauth/tokens')
.then(response => {
this.tokens = response.data;
});
},
/**
* Revoke the given token.
*/
revoke(token) {
axios.delete('oauth/tokens/' + token.id)
.then(response => {
this.getTokens();
});
}
}
};
</script>

View File

@@ -0,0 +1,316 @@
<template>
<div>
<notifications group="passport-clients" position="top middle" :duration="5000" width="400" />
<h3 class="mb3">
{{ $t('settings.api_oauth_clients') }}
<a class="btn nt2" :class="[ dirltr ? 'fr' : 'fl' ]" href="" @click.prevent="showCreateClientForm">
{{ $t('settings.api_oauth_create_new') }}
</a>
</h3>
<p>{{ $t('settings.api_oauth_clients_desc') }}</p>
<p v-html="$t('settings.api_oauth_clients_desc2', { url: 'https://laravel.com/docs/master/passport#requesting-tokens' })"></p>
<!-- Current Clients -->
<p v-if="clients.length === 0" class="mb0">
{{ $t('settings.api_oauth_not_created') }}
</p>
<div v-else class="dt w-100 collapse br--top br--bottom">
<em>{{ $t('settings.api_oauth_title') }}</em>
<div class="dt-row">
<div class="dtc w-20">
<div class="pa2 b">
{{ $t('settings.api_oauth_clientid') }}
</div>
</div>
<div class="dtc w-20">
<div class="pa2 b">
{{ $t('settings.api_oauth_name') }}
</div>
</div>
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.api_oauth_secret') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="client in clients" :key="client.id" class="dt-row bb b--light-gray">
<!-- ID -->
<div class="dtc">
<div class="pa2">
{{ client.id }}
</div>
</div>
<!-- Name -->
<div class="dtc">
<div class="pa2">
{{ client.name }}
</div>
</div>
<!-- Secret -->
<div class="dtc">
<div class="pa2 flex flex-auto">
<code dir="ltr">{{ client.secret }}</code>
<em class="fa fa-clipboard pointer" :class="[ dirltr ? 'ml2' : 'mr2' ]"
:title="$t('settings.dav_copy_help')"
@click="copyIntoClipboard(client.secret)"
></em>
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<em class="fa fa-pencil-square-o pointer pr2" @click="edit(client)"></em>
<em class="fa fa-trash-o pointer" @click="destroy(client)"></em>
</div>
</div>
</div>
</div>
<!-- Create Client Modal -->
<sweet-modal ref="modalClient" overlay-theme="dark" tabindex="-1" role="dialog"
:title="form.id ? $t('settings.api_oauth_edit') : $t('settings.api_oauth_create')"
@open="_focusInput"
>
<!-- Form Errors -->
<errors :errors="form.errors" />
<!-- Create Client Form -->
<form ref="form" class="form-horizontal" role="form">
<!-- Name -->
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'client-name'"
ref="clientName"
v-model="form.name"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_name')"
:validator="$v.form.name"
@submit="store"
/>
<span class="help-block">
{{ $t('settings.api_oauth_name_help') }}
</span>
</div>
</div>
<!-- Redirect URL -->
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'redirect-url'"
v-model="form.redirect"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_oauth_redirecturl')"
:validator="$v.form.redirect"
@submit="store"
/>
<span class="help-block">
{{ $t('settings.api_oauth_redirecturl_help') }}
</span>
</div>
</div>
</form>
<!-- Modal Actions -->
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.close') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="store">
{{ form.id ? $t('app.save') : $t('app.create') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import Errors from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required, url } from 'vuelidate/lib/validators';
export default {
components: {
SweetModal,
Errors,
},
mixins: [validationMixin],
data() {
return {
clients: [],
form: {
errors: [],
name: '',
redirect: ''
},
};
},
validations: {
form: {
name: {
required,
},
redirect: {
required,
url,
}
}
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getClients();
},
/**
* Focus on modal open.
*/
_focusInput() {
const vm = this;
setTimeout(function() {
vm.$refs.clientName.focus();
}, 10);
},
/**
* Get all of the OAuth clients for the user.
*/
getClients() {
axios.get('oauth/clients')
.then(response => {
this.clients = response.data;
});
},
/**
* Show the form for creating new clients.
*/
showCreateClientForm() {
this.resetField();
this.$refs.modalClient.open();
},
/**
* Create a new OAuth client for the user.
*/
store() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
const method = this.form.id ? 'put' : 'post';
const url = this.form.id ? 'oauth/clients/' + this.form.id : 'oauth/clients';
this.persistClient(method, url, this.form);
},
/**
* Edit the given client.
*/
edit(client) {
this.form = Object.assign({errors:[]}, client);
this.$refs.modalClient.open();
},
/**
* Persist the client to storage using the given form.
*/
persistClient(method, uri, form) {
form.errors = [];
axios[method](uri, form)
.then(response => {
this.getClients();
this.closeModal();
})
.catch(error => {
if (typeof error.response.data === 'object') {
form.errors = _.flatten(_.toArray(error.response.data));
} else {
form.errors = [this.$t('app.error_try_again')];
}
});
},
/**
* Destroy the given client.
*/
destroy(client) {
axios.delete('oauth/clients/' + client.id)
.then(response => {
this.getClients();
});
},
closeModal() {
this.resetField();
this.$v.$reset();
this.$refs.form.reset();
this.$refs.modalClient.close();
},
resetField() {
this.form = {
id: '',
errors:[],
name: '',
redirect: '',
};
},
/**
* Copy text into clipboard
*/
copyIntoClipboard(text) {
this.$copyText(text)
.then(response => {
this.notify(this.$t('settings.dav_clipboard_copied'), true);
});
},
notify(text, success) {
this.$notify({
group: 'passport-clients',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,338 @@
<style scoped>
.access-key {
border: 1px solid #cacaca;
border-radius: 3px;
padding: 10px 10px 0;
background-color: #fafafa;
}
pre {
font-size: 12px;
word-wrap: break-word;
white-space: pre-wrap;
}
</style>
<template>
<div>
<h3 class="mb3">
{{ $t('settings.api_personal_access_tokens') }}
<a class="btn nt2" :class="[ dirltr ? 'fr' : 'fl' ]" href="" @click.prevent="showCreateTokenForm">
{{ $t('settings.api_token_create_new') }}
</a>
</h3>
<p>{{ $t('settings.api_pao_description') }}</p>
<!-- No Tokens Notice -->
<p v-if="tokens.length === 0" class="mb0">
{{ $t('settings.api_token_not_created') }}
</p>
<div v-else class="dt w-75 collapse br--top br--bottom">
<em>{{ $t('settings.api_token_title') }}</em>
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.api_token_name') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="token in tokens" :key="token.id" class="dt-row bb b--light-gray">
<!-- Client Name -->
<div class="dtc">
<div v-tooltip="$t('settings.api_token_expire', { date: token.expires_at })" class="pa2">
{{ token.name }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<span class="pointer" @click="revoke(token)">{{ $t('app.delete') }}</span>
</div>
</div>
</div>
</div>
<!-- Create Token Modal -->
<sweet-modal ref="modalCreateToken" overlay-theme="dark" tabindex="-1" role="dialog"
:title="$t('settings.api_token_create')" @open="_focusInput"
>
<!-- Form Errors -->
<errors :errors="form.errors" />
<!-- Create Token Form -->
<form ref="form" class="form-horizontal" role="form" @submit.prevent="store">
<!-- Name -->
<div class="col-md-auto">
<form-input
:id="'create-token-name'"
ref="createTokenName"
v-model="form.name"
:name="'name'"
:iclass="'br2 f5 w-50 ba b--black-40 pa2 outline-0'"
:required="true"
:title="$t('settings.api_token_name')"
:validator="$v.form.name"
/>
</div>
<!-- Scopes -->
<div v-if="scopes.length > 0" class="form-group">
<label class="col-md-4 col-form-label">
{{ $t('settings.api_token_scopes') }}
</label>
<div class="col-md-auto">
<div v-for="scope in scopes" :key="scope.id">
<div class="checkbox">
<label>
<input type="checkbox"
:checked="scopeIsAssigned(scope.id)"
@click="toggleScope(scope.id)"
/>
{{ scope.id }}
</label>
</div>
</div>
</div>
</div>
</form>
<!-- Modal Actions -->
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.close') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.create') }}
</a>
</div>
</sweet-modal>
<!-- Access Token Modal -->
<sweet-modal ref="modalAccessToken" overlay-theme="dark" tabindex="-1" role="dialog" :title="$t('settings.api_token_title')">
<notifications group="passport-personal-access-token" position="middle" :duration="5000" width="400" />
<p>{{ $t('settings.api_token_help') }}</p>
<div class="flex-auto access-key overflow-y-scroll" style="max-height: 400px;" @click.prevent="copyIntoClipboard(accessToken)">
<pre><code>{{ accessToken }}</code></pre>
</div>
<!-- Modal Actions -->
<div slot="button">
<a class="btn btn-primary" :title="$t('settings.dav_copy_help')" href="" @click.prevent="copyIntoClipboard(accessToken)">
{{ $t('app.copy') }}
</a>
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.close') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import Errors from '../partials/Error.vue';
import { SweetModal } from 'sweet-modal-vue';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
export default {
components: {
SweetModal,
Errors,
},
mixins: [validationMixin],
data() {
return {
endpoint: '',
accessToken: null,
tokens: [],
scopes: [],
form: {
name: '',
scopes: [],
errors: []
},
};
},
validations: {
form: {
name: {
required,
}
}
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
//this.$refs.modalAccessToken.$refs.content.className = 'flex-auto';
this.$refs.modalAccessToken.$refs.content.getElementsByClassName('sweet-content-content')[0].className = 'flex-auto';
this.getTokens();
this.getScopes();
},
/**
* Get all of the personal access tokens for the user.
*/
getTokens() {
axios.get('oauth/personal-access-tokens')
.then(response => {
this.tokens = response.data;
});
},
/**
* Get all of the available scopes.
*/
getScopes() {
axios.get('oauth/scopes')
.then(response => {
this.scopes = response.data;
});
},
/**
* Close all modals.
*/
closeModal() {
this.$refs.form.reset();
this.$v.$reset();
this.$refs.modalCreateToken.close();
this.$refs.modalAccessToken.close();
},
/**
* Focus on modal open.
*/
_focusInput() {
const vm = this;
setTimeout(function() {
vm.$refs.createTokenName.focus();
}, 10);
},
/**
* Show the form for creating new tokens.
*/
showCreateTokenForm() {
this.$refs.modalCreateToken.open();
},
/**
* Create a new personal access token.
*/
store() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
this.accessToken = null;
this.form.errors = [];
axios.post('oauth/personal-access-tokens', this.form)
.then(response => {
this.form.name = '';
this.form.scopes = [];
this.form.errors = [];
this.tokens.push(response.data.token);
this.showAccessToken(response.data.accessToken);
})
.catch(error => {
if (typeof error.response.data === 'object') {
this.form.errors = _.flatten(_.toArray(error.response.data));
} else {
this.form.errors = [this.$t('app.error_try_again')];
}
});
},
/**
* Toggle the given scope in the list of assigned scopes.
*/
toggleScope(scope) {
if (this.scopeIsAssigned(scope)) {
this.form.scopes = _.reject(this.form.scopes, s => s === scope);
} else {
this.form.scopes.push(scope);
}
},
/**
* Determine if the given scope has been assigned to the token.
*/
scopeIsAssigned(scope) {
return _.indexOf(this.form.scopes, scope) >= 0;
},
/**
* Show the given access token to the user.
*/
showAccessToken(accessToken) {
this.$refs.modalCreateToken.close();
this.accessToken = accessToken;
this.$refs.modalAccessToken.open();
},
/**
* Revoke the given token.
*/
revoke(token) {
axios.delete('oauth/personal-access-tokens/' + token.id)
.then(response => {
this.getTokens();
});
},
/**
* Copy text into clipboard
*/
copyIntoClipboard(text) {
this.$copyText(text)
.then(response => {
this.notify(this.$t('settings.dav_clipboard_copied'), true);
});
},
notify(text, success) {
this.$notify({
group: 'passport-personal-access-token',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

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>

View File

@@ -0,0 +1,478 @@
<template>
<div>
<notifications group="activityTypes" position="bottom right" />
<h3 class="with-actions">
{{ $t('settings.personalization_activity_type_category_title') }}
<a v-if="!limited" v-cy-name="'add-activity-type-category-button'" class="btn nt2" :class="[ dirltr ? 'fr' : 'fl' ]" href=""
@click.prevent="showCreateCategoryModal"
>
{{ $t('settings.personalization_activity_type_category_add') }}
</a>
</h3>
<p>{{ $t('settings.personalization_activity_type_category_description') }}</p>
<div v-if="limited" v-cy-name="'activity-type-premium-message'" 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>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_activity_type_category_table_name') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_activity_type_category_table_actions') }}
</div>
</div>
</div>
</div>
<div v-cy-name="'activity-types'">
<ul v-cy-name="'activity-type-categories'" v-cy-items="activityTypeCategories.map(a => a.id)">
<li v-for="activityTypeCategory in activityTypeCategories" :key="activityTypeCategory.id" v-cy-name="'activity-types-'+activityTypeCategory.id"
v-cy-items="activityTypeCategory.activityTypes ? activityTypeCategory.activityTypes.map(a => a.id) : ''" class="dt dt--fixed w-100 collapse br--top br--bottom mt3"
>
<!-- ACTIVITY TYPE CATEGORY -->
<div class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2 b">
<strong>{{ activityTypeCategory.name }}</strong>
</div>
</div>
<div class="dtc">
<div class="pa2" :class="[ dirltr ? 'tr' : 'tl' ]">
<em v-if="!limited" v-cy-name="'activity-type-category-edit-button-'+activityTypeCategory.id"
class="fa fa-pencil-square-o pointer pr2" @click="showEditCategory(activityTypeCategory)"
></em>
<em v-if="!limited" v-cy-name="'activity-type-category-delete-button-'+activityTypeCategory.id"
class="fa fa-trash-o pointer" @click="showDeleteCategory(activityTypeCategory)"
></em>
</div>
</div>
</div>
<div v-for="activityType in activityTypeCategory.activityTypes" :key="activityType.id" class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2 pl4">
{{ activityType.name }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<em v-if="!limited" v-cy-name="'activity-type-edit-button-'+activityType.id"
class="fa fa-pencil-square-o pointer pr2" @click="showEditType(activityType, activityTypeCategory.id)"
></em>
<em v-if="!limited" v-cy-name="'activity-type-delete-button-'+activityType.id"
class="fa fa-trash-o pointer" @click="showDeleteType(activityType)"
></em>
</div>
</div>
</div>
<div v-if="!limited" class="dt-row">
<div class="dtc">
<div class="pa2 pl4">
<a v-cy-name="'add-activity-type-button-for-category-'+activityTypeCategory.id" class="pointer" href=""
@click.prevent="showCreateTypeModal(activityTypeCategory)"
>
{{ $t('settings.personalization_activity_type_add_button') }}
</a>
</div>
</div>
</div>
</li>
</ul>
</div>
<!-- Create Activity Type Category -->
<sweet-modal ref="createCategoryModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_category_modal_add')">
<form @submit.prevent="storeCategory()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'add-category-name'"
v-model="createCategoryForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_activity_type_category_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeCategoryModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'add-activity-type-category-save-button'" class="btn btn-primary" href="" @click.prevent="storeCategory()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
<!-- Update Activity Type Category -->
<sweet-modal ref="updateCategoryModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_category_modal_edit')">
<form @submit.prevent="updateCategory()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'update-category-name'"
v-model="updateCategoryForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_activity_type_category_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeUpdateCategoryModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'update-activity-type-category-button'" class="btn btn-primary" href="" @click.prevent="updateCategory()">
{{ $t('app.update') }}
</a>
</div>
</sweet-modal>
<!-- Create Activity Type -->
<sweet-modal ref="createTypeModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_modal_add')">
<form @submit.prevent="storeType()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'add-type-name'"
v-model="createTypeForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_activity_type_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeCreateTypeModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'add-type-button'" class="btn btn-primary" href="" @click.prevent="storeType()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
<!-- Update Activity Type -->
<sweet-modal ref="updateTypeModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_modal_edit')">
<form @submit.prevent="updateType()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'update-type-name'"
v-model="updateTypeForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_activity_type_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeUpdateTypeModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'update-type-button'" class="btn btn-primary" href="" @click.prevent="updateType()">
{{ $t('app.update') }}
</a>
</div>
</sweet-modal>
<!-- Delete Activiy type category -->
<sweet-modal ref="deleteCategoryModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_category_modal_delete')">
<form>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
</div>
</div>
<div class="mb4">
<p class="mb2">
{{ $t('settings.personalization_activity_type_category_modal_delete_desc') }}
</p>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteCategoryModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'delete-category-button'" class="btn btn-primary" href="" @click.prevent="destroyCategory()">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
<!-- Delete Activiy type -->
<sweet-modal ref="deleteTypeModal" overlay-theme="dark" :title="$t('settings.personalization_activity_type_modal_delete')">
<form>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
</div>
</div>
<div class="mb4">
<p class="mb2">
{{ $t('settings.personalization_activity_type_modal_delete_desc') }}
</p>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteTypeModal()">
{{ $t('app.cancel') }}
</a>
<a v-cy-name="'delete-type-button'" class="btn btn-primary" href="" @click.prevent="destroyType()">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal
},
props: {
limited: {
type: Boolean,
default: false,
},
},
data() {
return {
activityTypes: [],
activityTypeCategories: [],
errorMessage: '',
updatedCategory: {
id: '',
name: ''
},
createCategoryForm: {
name: '',
errors: []
},
createTypeForm: {
name: '',
activity_type_category_id: '',
errors: []
},
updateCategoryForm: {
id: '',
name: '',
activity_type_category_id: '',
errors: []
},
updateTypeForm: {
id: '',
name: '',
errors: []
},
destroyCategoryForm: {
id: '',
errors: []
},
destroyTypeForm: {
id: '',
errors: []
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getActivityTypeCategories();
},
getActivityTypeCategories() {
axios.get('settings/personalization/activitytypecategories')
.then(response => {
this.activityTypeCategories = response.data;
});
},
closeCategoryModal() {
this.$refs.createCategoryModal.close();
},
closeDeleteCategoryModal() {
this.$refs.deleteCategoryModal.close();
},
showCreateCategoryModal() {
this.$refs.createCategoryModal.open();
},
storeCategory() {
axios.post('settings/personalization/activitytypecategories', this.createCategoryForm)
.then(response => {
this.$refs.createCategoryModal.close();
this.activityTypeCategories.push(response.data.data);
this.createCategoryForm.name = '';
this.notify(this.$t('app.default_save_success'), true);
});
},
showEditCategory(category) {
this.updateCategoryForm.id = category.id;
this.updateCategoryForm.name = category.name;
this.updatedCategory = category;
this.$refs.updateCategoryModal.open();
},
showDeleteCategory(category) {
this.destroyCategoryForm.id = category.id;
this.$refs.deleteCategoryModal.open();
},
showDeleteType(type) {
this.destroyTypeForm.id = type.id;
this.$refs.deleteTypeModal.open();
},
showEditType(type, categoryId) {
this.updateTypeForm.id = type.id;
this.updateTypeForm.name = type.name;
this.updateTypeForm.activity_type_category_id = categoryId;
this.$refs.updateTypeModal.open();
},
closeUpdateCategoryModal() {
this.$refs.updateCategoryModal.close();
},
closeCreateTypeModal() {
this.$refs.createTypeModal.close();
},
closeUpdateTypeModal() {
this.$refs.updateTypeModal.close();
},
closeDeleteTypeModal() {
this.$refs.deleteTypeModal.close();
},
updateCategory() {
axios.put('settings/personalization/activitytypecategories/' + this.updateCategoryForm.id, this.updateCategoryForm)
.then(response => {
this.$refs.updateCategoryModal.close();
this.updatedCategory.name = this.updateCategoryForm.name;
this.updateCategoryForm.name = '';
this.notify(this.$t('app.default_save_success'), true);
});
},
showCreateTypeModal(category) {
this.$refs.createTypeModal.open();
this.createTypeForm.activity_type_category_id = category.id;
},
storeType() {
axios.post('settings/personalization/activitytypes', this.createTypeForm)
.then(response => {
this.$refs.createTypeModal.close();
this.activityTypes.push(response.data);
this.createTypeForm.name = '';
this.getActivityTypeCategories();
this.notify(this.$t('app.default_save_success'), true);
});
},
destroyCategory() {
axios.delete('settings/personalization/activitytypecategories/' + this.destroyCategoryForm.id)
.then(response => {
this.$refs.deleteCategoryModal.close();
this.destroyCategoryForm.id = '';
this.getActivityTypeCategories();
this.notify(this.$t('app.default_save_success'), true);
})
.catch(error => {
this.errorMessage = error.response.data.message;
});
},
updateType() {
axios.put('settings/personalization/activitytypes/' + this.updateTypeForm.id, this.updateTypeForm)
.then(response => {
this.$refs.updateTypeModal.close();
this.updatedCategory.name = this.updateTypeForm.name;
this.updateTypeForm.name = '';
this.getActivityTypeCategories();
this.notify(this.$t('app.default_save_success'), true);
});
},
destroyType() {
axios.delete('settings/personalization/activitytypes/' + this.destroyTypeForm.id)
.then(response => {
this.$refs.deleteTypeModal.close();
this.destroyTypeForm.id = '';
this.getActivityTypeCategories();
this.notify(this.$t('app.default_save_success'), true);
})
.catch(error => {
this.errorMessage = error.response.data.message;
});
},
notify(text, success) {
this.$notify({
group: 'activityTypes',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,392 @@
<template>
<div>
<notifications group="main" position="bottom right" />
<h3 class="with-actions">
{{ $t('settings.personalization_contact_field_type_title') }}
<a class="btn nt2" :class="[ dirltr ? 'fr' : 'fl' ]" href="" @click.prevent="add">
{{ $t('settings.personalization_contact_field_type_add') }}
</a>
</h3>
<p>{{ $t('settings.personalization_contact_field_type_description') }}</p>
<div v-if="submitted" class="pa2 ba b--yellow mb3 mt3 br2 bg-washed-yellow">
{{ $t('settings.personalization_contact_field_type_add_success') }}
</div>
<div v-if="edited" class="pa2 ba b--yellow mb3 mt3 br2 bg-washed-yellow">
{{ $t('settings.personalization_contact_field_type_edit_success') }}
</div>
<div v-if="deleted" class="pa2 ba b--yellow mb3 mt3 br2 bg-washed-yellow">
{{ $t('settings.personalization_contact_field_type_delete_success') }}
</div>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_name') }}
</div>
</div>
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_protocol') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="contactFieldType in contactFieldTypes" :key="contactFieldType.id" class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2">
<em v-if="contactFieldType.fontawesome_icon" :class="contactFieldType.fontawesome_icon" class="pr2"></em>
<em v-else class="pr2 fa fa-address-card-o"></em>
{{ contactFieldType.name }}
</div>
</div>
<div class="dtc">
<code class="f7">
{{ contactFieldType.protocol }}
</code>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<em class="fa fa-pencil-square-o pointer pr2" @click="edit(contactFieldType)"></em>
<em v-if="contactFieldType.delible" class="fa fa-trash-o pointer" @click="showDelete(contactFieldType)"></em>
</div>
</div>
</div>
</div>
<!-- Create Contact field type -->
<sweet-modal ref="modalCreateContactFieldType" overlay-theme="dark"
:title="$t('settings.personalization_contact_field_type_modal_title')"
@open="_focusCreateInput"
>
<!-- Form Errors -->
<errors :errors="createForm.errors" />
<form class="form-horizontal" role="form" @submit.prevent="store">
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'name'"
ref="createName"
v-model="createForm.name"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_name')"
@submit="store"
/>
</div>
</div>
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'protocol'"
v-model="createForm.protocol"
:placeholder="'mailto:'"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_protocol')"
@submit="store"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_contact_field_type_modal_protocol_help') }}
</small>
</div>
</div>
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'icon'"
v-model="createForm.icon"
:placeholder="'fa fa-address-book-o'"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_icon')"
@submit="store"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_contact_field_type_modal_icon_help') }}
</small>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="store">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
<!-- Edit Contact field type -->
<sweet-modal ref="modalEditContactFieldType" overlay-theme="dark"
:title="$t('settings.personalization_contact_field_type_modal_edit_title')"
@open="_focusEditInput"
>
<!-- Form Errors -->
<error :errors="editForm.errors" />
<form class="form-horizontal" role="form" @submit.prevent="update">
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'name'"
ref="editName"
v-model="editForm.name"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_name')"
@submit="update"
/>
</div>
</div>
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'protocol'"
v-model="editForm.protocol"
:placeholder="'mailto:'"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_protocol')"
@submit="update"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_contact_field_type_modal_protocol_help') }}
</small>
</div>
</div>
<div class="form-group">
<div class="col-md-auto">
<form-input
:id="'icon'"
v-model="editForm.icon"
:placeholder="'fa fa-address-book-o'"
:required="true"
:title="$t('settings.personalization_contact_field_type_modal_icon')"
@submit="update"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_contact_field_type_modal_icon_help') }}
</small>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="update">
{{ $t('app.edit') }}
</a>
</div>
</sweet-modal>
<!-- Delete Contact field type -->
<sweet-modal ref="modalDeleteContactFieldType" overlay-theme="dark"
:title="$t('settings.personalization_contact_field_type_modal_delete_title')"
>
<p>
{{ $t('settings.personalization_contact_field_type_modal_delete_description') }}
</p>
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="trash">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
import Errors from '../partials/Error.vue';
export default {
components: {
SweetModal,
Errors,
},
data() {
return {
contactFieldTypes: [],
submitted: false,
edited: false,
deleted: false,
createForm: {
name: '',
protocol: '',
icon: '',
errors: []
},
editForm: {
id: '',
name: '',
protocol: '',
icon: '',
errors: []
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getContactFieldTypes();
},
getContactFieldTypes() {
axios.get('settings/personalization/contactfieldtypes')
.then(response => {
this.contactFieldTypes = response.data;
});
},
add() {
this.$refs.modalCreateContactFieldType.open();
},
closeModal() {
this.$refs.modalCreateContactFieldType.close();
this.$refs.modalEditContactFieldType.close();
this.$refs.modalDeleteContactFieldType.close();
},
store() {
this.persistClient(
'post', 'settings/personalization/contactfieldtypes',
this.createForm, this.submitted
);
this.$notify({
group: 'main',
title: this.$t('settings.personalization_contact_field_type_add_success'),
text: '',
width: '500px',
type: 'success'
});
},
edit(contactFieldType) {
this.editForm.id = contactFieldType.id;
this.editForm.name = contactFieldType.name;
this.editForm.protocol = contactFieldType.protocol;
this.editForm.icon = contactFieldType.fontawesome_icon;
this.$refs.modalEditContactFieldType.open();
},
update() {
this.persistClient(
'put', 'settings/personalization/contactfieldtypes/' + this.editForm.id,
this.editForm, this.edited
);
this.$notify({
group: 'main',
title: this.$t('settings.personalization_contact_field_type_edit_success'),
text: '',
width: '500px',
type: 'success'
});
},
showDelete(contactFieldType) {
this.editForm.id = contactFieldType.id;
this.$refs.modalDeleteContactFieldType.open();
},
trash() {
this.persistClient(
'delete', 'settings/personalization/contactfieldtypes/' + this.editForm.id,
this.editForm, this.deleted
);
this.$notify({
group: 'main',
title: this.$t('settings.personalization_contact_field_type_delete_success'),
text: '',
width: '500px',
type: 'success'
});
},
persistClient(method, uri, form, success) {
form.errors = {};
axios[method](uri, form)
.then(response => {
this.getContactFieldTypes();
form.id = '';
form.name = '';
form.protocol = '';
form.icon = '';
form.errors = [];
this.closeModal();
success = true;
})
.catch(error => {
if (typeof error.response.data === 'object') {
form.errors = _.flatten(_.toArray(error.response.data));
} else {
form.errors = [this.$t('app.error_try_again')];
}
});
},
/**
* Focus on modal open.
*/
_focusCreateInput() {
const vm = this;
setTimeout(function() {
vm.$refs.createName.focus();
}, 10);
},
/**
* Focus on modal open.
*/
_focusEditInput() {
const vm = this;
setTimeout(function() {
vm.$refs.editName.focus();
}, 10);
},
}
};
</script>

View File

@@ -0,0 +1,128 @@
<style scoped>
.url {
width: 650px;
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
}
</style>
<template>
<div>
<h2>{{ $t('settings.dav_title') }}</h2>
<p>{{ $t('settings.dav_description') }}</p>
<notifications group="dav" position="top middle" :duration="5000" width="400" />
<div class="form-group">
<label for="dav_url_base">
{{ $t('settings.dav_url_base') }}
</label>
<br />
<input id="dav_url_base" :value="davRoute" class="url form-control" type="text" readonly />
<a class="btn btn-primary" :title="$t('settings.dav_copy_help')" href="" @click.prevent="copyIntoClipboard(davRoute)">
{{ $t('app.copy') }}
</a>
<p>
{{ $t('settings.dav_connect_help') }}
</p>
<p>
{{ $t('settings.dav_connect_help2') }}
</p>
</div>
<div class="settings-group">
<h2>{{ $t('settings.dav_title_carddav') }}</h2>
<div class="form-group">
<label for="dav_url_carddav">
{{ $t('settings.dav_url_carddav') }}
</label>
<br />
<input id="dav_url_carddav" :value="cardDavRoute" class="url form-control" type="text" readonly />
<a class="btn btn-primary" :title="$t('settings.dav_copy_help')" href="" @click.prevent="copyIntoClipboard(cardDavRoute)">
{{ $t('app.copy') }}
</a>
<br />
<a :href="cardDavRoute+'?export'">
{{ $t('settings.dav_carddav_export') }}
</a>
</div>
</div>
<div class="settings-group">
<h2>{{ $t('settings.dav_title_caldav') }}</h2>
<div class="form-group">
<label for="dav_url_caldav_birthdays">
{{ $t('settings.dav_url_caldav_birthdays') }}
</label>
<br />
<input id="dav_url_caldav_birthdays" :value="calDavBirthdaysRoute" class="url form-control" type="text" readonly />
<a class="btn btn-primary" :title="$t('settings.dav_copy_help')" href="" @click.prevent="copyIntoClipboard(calDavBirthdaysRoute)">
{{ $t('app.copy') }}
</a>
<br />
<a :href="calDavBirthdaysRoute+'?export'">
{{ $t('settings.dav_caldav_birthdays_export') }}
</a>
</div>
<div class="form-group">
<label for="dav_url_caldav_tasks">
{{ $t('settings.dav_url_caldav_tasks') }}
</label>
<br />
<input id="dav_url_caldav_tasks" :value="calDavTasksRoute" class="url form-control" type="text" readonly />
<a class="btn btn-primary" :title="$t('settings.dav_copy_help')" href="" @click.prevent="copyIntoClipboard(calDavTasksRoute)">
{{ $t('app.copy') }}
</a>
<br />
<a :href="calDavTasksRoute+'?export'">
{{ $t('settings.dav_caldav_tasks_export') }}
</a>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
davRoute: {
type: String,
default: '',
},
cardDavRoute: {
type: String,
default: '',
},
calDavBirthdaysRoute: {
type: String,
default: '',
},
calDavTasksRoute: {
type: String,
default: '',
},
},
methods: {
copyIntoClipboard(text) {
this.$copyText(text)
.then(response => {
this.notify(this.$t('settings.dav_clipboard_copied'), true);
});
},
notify(text, success) {
this.$notify({
group: 'dav',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,443 @@
<template>
<div>
<notifications group="main" position="bottom right" />
<h3 class="mb3">
{{ $t('settings.personalization_genders_title') }}
<a class="btn nt2" :class="[ dirltr ? 'fr' : 'fl' ]" href="" @click.prevent="showCreateModal">
{{ $t('settings.personalization_genders_add') }}
</a>
</h3>
<p>{{ $t('settings.personalization_genders_desc') }}</p>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_genders_table_name') }}
</div>
</div>
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_genders_table_sex') }}
</div>
</div>
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_genders_table_default') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="gender in genders" :key="gender.id"
class="dt-row bb b--light-gray"
>
<div class="dtc">
<div class="pa2">
{{ gender.name }}
<span class="i">
{{ $tc('settings.personalization_genders_list_contact_number', gender.numberOfContacts, { count: gender.numberOfContacts }) }}
</span>
</div>
</div>
<div class="dtc">
<div class="pa2">
{{ $t('settings.personalization_genders_' + gender.type.toLowerCase()) }}
</div>
</div>
<div class="dtc">
<div class="pa2">
<template v-if="gender.isDefault">
{{ $t('settings.personalization_genders_default') }}
</template>
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<em class="fa fa-pencil-square-o pointer pr2" @click="showEdit(gender)"></em>
<em v-if="genders.length > 1" class="fa fa-trash-o pointer" @click="showDelete(gender)"></em>
</div>
</div>
</div>
</div>
<div class="mt2" :class="[ dirltr ? 'tr' : 'tl' ]">
<a class="pointer" href="" @click.prevent="showDefaultGenderModal">{{ $t('settings.personalization_genders_make_default') }}</a>
</div>
<!-- Create Gender type -->
<sweet-modal ref="createModal" overlay-theme="dark" :title="$t('settings.personalization_genders_modal_add')">
<form @submit.prevent="store()">
<div class="form-group">
<div class="form-group">
<form-input
:id="''"
v-model="createForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_genders_modal_name')"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_genders_modal_name_help') }}
</small>
</div>
<div class="form-group">
<form-select
:id="''"
v-model="createForm.type"
:options="genderTypes"
:required="true"
:title="$t('settings.personalization_genders_modal_sex')"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_genders_modal_sex_help') }}
</small>
</div>
<div class="form-group">
<form-toggle
:id="''"
v-model="createForm.isDefault"
:labels="toggleOptions"
:required="true"
:title="$t('settings.personalization_genders_modal_default')"
/>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="store()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
<!-- Edit gender type -->
<sweet-modal ref="updateModal" overlay-theme="dark" :title="$t('settings.personalization_genders_modal_edit')">
<form @submit.prevent="update(updatedGender)">
<div class="form-group">
<div class="form-group">
<form-input
:id="''"
v-model="updateForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_genders_modal_name')"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_genders_modal_name_help') }}
</small>
</div>
<div class="form-group">
<form-select
:id="''"
v-model="updateForm.type"
:options="genderTypes"
:required="true"
:title="$t('settings.personalization_genders_modal_sex')"
/>
<small class="form-text text-muted">
{{ $t('settings.personalization_genders_modal_sex_help') }}
</small>
</div>
<div class="form-group">
<form-toggle
:id="''"
v-model="updateForm.isDefault"
:labels="toggleOptions"
:required="true"
:title="$t('settings.personalization_genders_modal_default')"
/>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeUpdateModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="update(updatedGender)">
{{ $t('app.update') }}
</a>
</div>
</sweet-modal>
<!-- Delete Gender type -->
<sweet-modal ref="deleteModal" overlay-theme="dark" :title="$t('settings.personalization_genders_modal_delete')">
<form>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
</div>
</div>
<div class="mb4">
<p class="mb2">
{{ $t('settings.personalization_genders_modal_delete_desc', {name: deleteForm.name}) }}
</p>
<div v-if="deleteForm.numberOfContacts !== 0 || deleteForm.isDefault">
<p v-if="deleteForm.numberOfContacts !== 0">
{{ $tc('settings.personalization_genders_modal_delete_question', deleteForm.numberOfContacts, {count: deleteForm.numberOfContacts}) }}
</p>
<p v-else>
{{ $tc('settings.personalization_genders_modal_delete_question_default') }}
</p>
<form-select
:id="'deleteNewId'"
v-model="deleteForm.newId"
:options="genders"
:required="true"
:title="''"
:excluded-id="deleteForm.id"
/>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteModal()">
{{ $t('app.cancel') }}
</a>
<a v-if="deleteForm.numberOfContacts === 0 && ! deleteForm.isDefault"
class="btn btn-primary"
href=""
@click.prevent="trash()"
>
{{ $t('app.delete') }}
</a>
<a v-else class="btn btn-primary" href="" @click.prevent="trashAndReplace()">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
<!-- Change default Gender -->
<sweet-modal ref="defaultGenderModal" overlay-theme="dark" :title="$t('settings.personalization_genders_modal_default')">
<form>
<div class="form-group">
<div class="form-group">
<form-select
:id="''"
v-model="defaultGenderId"
:options="genders"
:required="true"
:title="$t('settings.personalization_genders_select_default')"
/>
</div>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDefaultGenderModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="updateDefaultGender()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal
},
data() {
return {
genders: [],
genderTypes: [],
updatedGender: {
id: '',
name: '',
type: '',
isDefault: false,
},
errorMessage: '',
createForm: {
name: '',
type: '',
isDefault: false,
errors: []
},
updateForm: {
id: '',
name: '',
type: '',
isDefault: false,
errors: []
},
deleteForm: {
id: '',
name: '',
isDefault: false,
numberOfContacts: 0,
newId: 0
},
defaultGenderId: null,
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
toggleOptions() {
return {
checked: this.$t('app.yes'),
unchecked: this.$t('app.no')
};
},
defaultGenderType() {
const defaultGender = _.findIndex(this.genders, ['isDefault', true]);
return this.genders[defaultGender >= 0 ? defaultGender : 0];
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
axios.all([
this.getGenders(),
this.getGenderTypes()
]);
},
setDefaultGenderType() {
this.createForm.type = this.defaultGenderType.type;
},
getGenders() {
return axios.get('settings/personalization/genders')
.then(response => {
this.genders = _.toArray(response.data);
this.setDefaultGenderType();
});
},
getGenderTypes() {
return axios.get('settings/personalization/genderTypes')
.then(response => {
this.genderTypes = _.toArray(response.data);
});
},
closeModal() {
this.$refs.createModal.close();
},
closeUpdateModal() {
this.$refs.updateModal.close();
},
closeDeleteModal() {
this.$refs.deleteModal.close();
},
closeDefaultGenderModal() {
this.$refs.defaultGenderModal.close();
},
showCreateModal() {
this.$refs.createModal.open();
},
showDefaultGenderModal() {
this.defaultGenderId = this.defaultGenderType.id;
this.$refs.defaultGenderModal.open();
},
store() {
axios.post('settings/personalization/genders', this.createForm)
.then(response => {
this.closeModal();
this.getGenders();
this.createForm.name = '';
this.createForm.type = '';
});
},
showEdit(gender) {
this.updateForm.id = gender.id.toString();
this.updateForm.name = gender.name;
this.updateForm.type = gender.type;
this.updateForm.isDefault = gender.isDefault;
this.updatedGender = gender;
this.$refs.updateModal.open();
},
update() {
axios.put('settings/personalization/genders/' + this.updateForm.id, this.updateForm)
.then(response => {
this.closeUpdateModal();
this.getGenders();
this.updatedGender.name = this.updateForm.name;
this.updatedGender.type = this.updateForm.type;
this.updatedGender.isDefault = this.updateForm.isDefault;
this.updateForm.name = '';
});
},
showDelete(gender) {
this.errorMessage = '';
this.deleteForm.name = gender.name;
this.deleteForm.id = gender.id.toString();
this.deleteForm.isDefault = gender.isDefault;
this.deleteForm.numberOfContacts = gender.numberOfContacts;
this.$refs.deleteModal.open();
},
trash() {
axios.delete('settings/personalization/genders/' + this.deleteForm.id)
.then(response => {
this.closeDeleteModal();
this.getGenders();
});
},
trashAndReplace() {
axios.delete('settings/personalization/genders/' + this.deleteForm.id + '/replaceby/' + this.deleteForm.newId)
.then(response => {
this.closeDeleteModal();
this.getGenders();
})
.catch(error => {
if (typeof error.response.data === 'object') {
this.errorMessage = error.response.data.message;
} else {
this.errorMessage = this.$t('app.error_try_again');
}
});
},
updateDefaultGender() {
axios.put('settings/personalization/genders/default/' + this.defaultGenderId)
.then(response => {
this.closeDefaultGenderModal();
this.getGenders();
});
}
}
};
</script>

View File

@@ -0,0 +1,300 @@
<template>
<div>
<notifications group="lifeEventTypes" position="bottom right" />
<h3 class="with-actions">
{{ $t('settings.personalization_life_event_category_title') }}
</h3>
<p>{{ $t('settings.personalization_life_event_category_description') }}</p>
<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>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_live_event_category_table_name') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_live_event_category_table_actions') }}
</div>
</div>
</div>
</div>
<div>
<ul>
<li v-for="lifeEventCategory in lifeEventCategories" :key="lifeEventCategory.id" class="dt dt--fixed w-100 collapse br--top br--bottom mt3">
<!-- LIFE EVENT CATEGORY -->
<div class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2 b">
<strong>{{ $t('people.life_event_category_' + lifeEventCategory.default_life_event_category_key) }}</strong>
</div>
</div>
<div class="dtc">
</div>
</div>
<div v-for="lifeEventType in lifeEventCategory.lifeEventTypes" :key="lifeEventType.id" class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2 pl4">
<template v-if="lifeEventType.name">
{{ lifeEventType.name }}
</template>
<template v-else>
{{ $t('people.life_event_sentence_' + lifeEventType.default_life_event_type_key) }}
</template>
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<em v-if="!limited" class="fa fa-pencil-square-o pointer pr2" @click="showEditType(lifeEventType, lifeEventCategory.id)"></em>
<em v-if="!limited" class="fa fa-trash-o pointer" @click="showDeleteType(lifeEventType)"></em>
</div>
</div>
</div>
<div v-if="!limited" class="dt-row">
<div class="dtc">
<div class="pa2 pl4">
<a class="pointer" href=""
@click.prevent="showCreateType(lifeEventCategory)"
>
{{ $t('settings.personalization_life_event_type_add_button') }}
</a>
</div>
</div>
</div>
</li>
</ul>
</div>
<!-- Create Life Event Type -->
<sweet-modal ref="createTypeModal" overlay-theme="dark" :title="$t('settings.personalization_life_event_type_modal_add')">
<form @submit.prevent="storeType()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'add-type-name'"
v-model="createTypeForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_life_event_type_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeCreateTypeModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="storeType()">
{{ $t('app.save') }}
</a>
</div>
</sweet-modal>
<!-- Update Life Event Type -->
<sweet-modal ref="updateTypeModal" overlay-theme="dark" :title="$t('settings.personalization_life_event_type_modal_edit')">
<form @submit.prevent="updateType()">
<div class="mb4">
<p class="b mb2"></p>
<form-input
:id="'update-type-name'"
v-model="updateTypeForm.name"
:input-type="'text'"
:required="true"
:title="$t('settings.personalization_life_event_type_modal_question')"
/>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeUpdateTypeModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="updateType()">
{{ $t('app.update') }}
</a>
</div>
</sweet-modal>
<!-- Delete Life Event type -->
<sweet-modal ref="deleteTypeModal" overlay-theme="dark" :title="$t('settings.personalization_life_event_type_modal_delete')">
<form>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
</div>
</div>
<div class="mb4">
<p class="mb2">
{{ $t('settings.personalization_life_event_type_modal_delete_desc') }}
</p>
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteTypeModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn btn-primary" href="" @click.prevent="destroyType()">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal
},
props: {
limited: {
type: Boolean,
default: false,
},
},
data() {
return {
lifeEventCategories: [],
errorMessage: '',
createTypeForm: {
name: '',
life_event_category_id: '',
errors: []
},
updateTypeForm: {
id: '',
name: '',
life_event_category_id: '',
errors: []
},
destroyTypeForm: {
id: '',
errors: []
},
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getLifeEventCategories();
},
getLifeEventCategories() {
axios.get('settings/personalization/lifeeventcategories')
.then(response => {
this.lifeEventCategories = response.data;
});
},
showCreateType(category) {
this.$refs.createTypeModal.open();
this.createTypeForm.life_event_category_id = category.id;
},
showDeleteType(type) {
this.destroyTypeForm.id = type.id;
this.$refs.deleteTypeModal.open();
},
showEditType(type, categoryId) {
this.updateTypeForm.id = type.id;
this.updateTypeForm.name = type.name ? type.name : this.$t('people.life_event_sentence_' + type.default_life_event_type_key);
this.updateTypeForm.life_event_category_id = categoryId;
this.$refs.updateTypeModal.open();
},
closeCreateTypeModal() {
this.$refs.createTypeModal.close();
},
closeUpdateTypeModal() {
this.$refs.updateTypeModal.close();
},
closeDeleteTypeModal() {
this.$refs.deleteTypeModal.close();
},
storeType() {
axios.post('settings/personalization/lifeeventtypes', this.createTypeForm)
.then(response => {
this.$refs.createTypeModal.close();
this.createTypeForm.name = '';
this.getLifeEventCategories();
this.notify(this.$t('app.default_save_success'), true);
});
},
updateType() {
axios.put('settings/personalization/lifeeventtypes/' + this.updateTypeForm.id, this.updateTypeForm)
.then(response => {
this.$refs.updateTypeModal.close();
this.updateTypeForm.name = '';
this.getLifeEventCategories();
this.notify(this.$t('app.default_save_success'), true);
});
},
destroyType() {
axios.delete('settings/personalization/lifeeventtypes/' + this.destroyTypeForm.id)
.then(response => {
this.$refs.deleteTypeModal.close();
this.destroyTypeForm.id = '';
this.getLifeEventCategories();
this.notify(this.$t('app.default_save_success'), true);
})
.catch(error => {
this.errorMessage = error.response.data.message;
});
},
notify(text, success) {
this.$notify({
group: 'lifeEventTypes',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,188 @@
<template>
<div>
<notifications group="mfa" position="bottom right" :duration="5000" width="400" />
<h3>{{ $t('settings.2fa_otp_title') }}</h3>
<div class="form-group">
<a v-if="selectActivated" class="btn btn-warning" href="" @click.prevent="showDisableModal">
{{ $t('settings.2fa_disable_title') }}
</a>
<a v-else class="btn btn-primary" href="" @click.prevent="showEnableModal">
{{ $t('settings.2fa_enable_title') }}
</a>
</div>
<sweet-modal id="enableModal" ref="enableModal" overlay-theme="dark" :title="$t('settings.2fa_otp_title')">
<form @submit.prevent="register()">
<p>{{ $t('settings.2fa_enable_description') }}</p>
<div class="panel-body">
{{ $t('settings.2fa_enable_otp') }}
<div v-html="image"></div>
<p>
{{ $t('settings.2fa_enable_otp_help') }}
<code id="secretkey">
{{ secret }}
</code>
</p>
</div>
<div class="form-group">
<p>
{{ $t('settings.2fa_enable_otp_validate') }}
</p>
<form-input
:id="'one_time_password1'"
v-model="one_time_password"
:title="$t('auth.2fa_one_time_password')"
:input-type="'number'"
:width="100"
:required="true"
/>
</div>
</form>
<div slot="button">
<a id="verify1" class="btn btn-primary" href="" @click.prevent="register()">
{{ $t('app.verify') }}
</a>
<a class="btn" href="" @click.prevent="closeEnableModal()">
{{ $t('app.cancel') }}
</a>
</div>
</sweet-modal>
<sweet-modal id="disableModal" ref="disableModal" overlay-theme="dark" :title="$t('settings.2fa_otp_title')">
<form @submit.prevent="register()">
<p>{{ $t('settings.2fa_disable_description') }}</p>
<div class="form-group">
<form-input
:id="'one_time_password2'"
v-model="one_time_password"
:title="$t('auth.2fa_one_time_or_recuperation')"
:input-type="'text'"
:width="100"
:required="true"
/>
</div>
</form>
<div slot="button">
<a id="verify2" class="btn btn-primary" href="" @click.prevent="unregister()">
{{ $t('app.verify') }}
</a>
<a class="btn" href="" @click.prevent="closeDisableModal()">
{{ $t('app.cancel') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal
},
props: {
activated: {
type: Boolean,
default: false
},
},
data() {
return {
selectActivated: false,
errorMessage: '',
infoMessage: '',
success: false,
one_time_password: '',
image: '',
secret: '',
};
},
watch: {
activated: function (val) {
this.selectActivated = val;
}
},
mounted() {
this.selectActivated = this.activated;
},
methods: {
register() {
axios.post('settings/security/2fa-enable', { one_time_password: this.one_time_password })
.then(response => {
this.closeEnableModal();
this.selectActivated = response.data.success;
if (response.data.success) {
this.notify(this.$t('settings.2fa_enable_success'), true);
} else {
this.notify(this.$t('settings.2fa_enable_error'), false);
}
}).catch(error => {
this.closeEnableModal();
this.notify(error.response.data.message, false);
});
},
unregister() {
axios.post('settings/security/2fa-disable', { one_time_password: this.one_time_password })
.then(response => {
this.closeDisableModal();
this.selectActivated = ! response.data.success;
if (response.data.success) {
this.notify(this.$t('settings.2fa_disable_success'), true);
} else {
this.notify(this.$t('settings.2fa_disable_error'), false);
}
}).catch(error => {
this.closeDisableModal();
this.notify(error.response.data.message, false);
});
},
showEnableModal() {
this.one_time_password = '';
axios.get('settings/security/2fa-enable')
.then(response => {
this.image = response.data.image;
this.secret = response.data.secret;
this.$refs.enableModal.open();
}).catch(error => {
this.notify(error.response.data.message, false);
});
},
showDisableModal() {
this.one_time_password = '';
this.$refs.disableModal.open();
},
closeEnableModal() {
this.$refs.enableModal.close();
},
closeDisableModal() {
this.$refs.disableModal.close();
},
notify(text, success) {
this.$notify({
group: 'mfa',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,109 @@
<template>
<div class="reminder-rules">
<notifications group="main" position="bottom right" />
<h3 class="mb3">
{{ $t('settings.personalization_module_title') }}
</h3>
<p>
{{ $t('settings.personalization_module_desc') }}
</p>
<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>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_name') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="module in modules" :key="module.id" class="dt-row hover bb b--light-gray">
<div class="dtc">
<div class="pa2">
{{ module.name }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<form-toggle
v-model="module.active"
:iclass="'module-'"
:disabled="limited"
:labels="true"
@change="toggle(module, $event)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
limited: {
type: Boolean,
default: false,
},
},
data() {
return {
modules: [],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.getModules();
},
methods: {
getModules() {
axios.get('settings/personalization/modules')
.then(response => {
this.modules = response.data;
});
},
toggle(module) {
axios.post('settings/personalization/modules/' + module.id)
.then(response => {
this.$notify({
group: 'main',
title: this.$t('settings.personalization_module_save'),
text: '',
type: 'success'
});
this.$set(module, 'active', response.data.data.active);
});
}
}
};
</script>

View File

@@ -0,0 +1,139 @@
<style scoped>
.code {
margin-bottom: 0.1em;
}
.used {
text-decoration: line-through;
}
</style>
<template>
<div>
<h3>{{ $t('settings.recovery_title') }}</h3>
<div class="form-group">
<a class="btn btn-primary" href="" @click.prevent="showRecoveryModal">
{{ $t('settings.recovery_show') }}
</a>
</div>
<sweet-modal id="recoveryModal" ref="recoveryModal" overlay-theme="dark" :title="$t('settings.recovery_title')">
<notifications group="recovery" position="top middle" :duration="5000" width="400" />
<p>{{ $t('settings.recovery_help_intro') }}</p>
<p :class="[ dirltr ? 'ml3' : 'mr3' ]">
<span v-for="code in codes" :key="code.id" v-cy-name="'recovery-' + code.id">
<pre class="code" :class="[ code.used ? 'used' : '' ]" :title="[ code.used ? usedHelp : '']">{{ code.recovery }}</pre>
</span>
</p>
<p>{{ $t('settings.recovery_help_information') }}</p>
<div slot="button">
<span :class="[ dirltr ? 'fl' : 'fr' ]">
<a class="btn" href="" @click.prevent="generateNewCodes">
{{ $t('settings.recovery_generate') }}
</a>
<br />
<small class="form-text text-muted">
{{ $t('settings.recovery_generate_help') }}
</small>
</span>
<span :class="[ dirltr ? 'fr' : 'fl' ]">
<a class="btn btn-primary" :title="copyHelp" href="" @click.prevent="copyIntoClipboard">
{{ $t('app.copy') }}
</a>
<!--
<a @click.prevent="download" class="btn" href="">{{ $t('app.download') }}</a>
-->
<a class="btn" href="" @click.prevent="closeRecoveryModal">
{{ $t('app.close') }}
</a>
</span>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
export default {
components: {
SweetModal
},
data() {
return {
codes: [],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
},
usedHelp() {
return this.$t('settings.recovery_already_used_help');
},
copyHelp() {
return this.$t('settings.recovery_copy_help');
},
},
methods: {
showRecoveryModal() {
this.codes = [];
axios.post('settings/security/recovery-codes')
.then(response => {
this.codes = response.data;
this.$refs.recoveryModal.open();
}).catch(error => {
this.notify(error.response.data.message, false);
});
},
generateNewCodes() {
this.codes = [];
axios.post('settings/security/generate-recovery-codes')
.then(response => {
this.codes = response.data;
}).catch(error => {
this.notify(error.response.data.message, false);
});
},
closeRecoveryModal() {
this.$refs.recoveryModal.close();
},
copyIntoClipboard() {
this.$copyText(this.getDataStream())
.then(response => {
this.notify(this.$t('settings.recovery_clipboard'), true);
});
},
getDataStream() {
var text = this.$t('settings.recovery_help_intro')+'\n';
var i = 1;
this.codes.forEach(code => {
if (code.used) {
text += i + '. ---------\n';
} else {
text += i + '. ' + code.recovery + '\n';
}
i++;
});
return text;
},
notify(text, success) {
this.$notify({
group: 'recovery',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="reminder-rules">
<notifications group="main" position="bottom right" />
<h3 class="mb3">
{{ $t('settings.personalization_reminder_rule_title') }}
</h3>
<p>
{{ $t('settings.personalization_reminder_rule_desc') }}
</p>
<div class="dt dt--fixed w-100 collapse br--top br--bottom">
<div class="dt-row">
<div class="dtc">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_name') }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2 b">
{{ $t('settings.personalization_contact_field_type_table_actions') }}
</div>
</div>
</div>
<div v-for="reminderRule in reminderRules" :key="reminderRule.id" class="dt-row bb b--light-gray">
<div class="dtc">
<div class="pa2">
{{ $tc('settings.personalization_reminder_rule_line', reminderRule.number_of_days_before, {count: reminderRule.number_of_days_before}) }}
</div>
</div>
<div class="dtc" :class="[ dirltr ? 'tr' : 'tl' ]">
<div class="pa2">
<form-toggle
v-model="reminderRule.active"
:iclass="'reminder-rule-' + reminderRule.number_of_days_before"
:labels="true"
@change="toggle(reminderRule, $event)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
reminderRules: [],
};
},
computed: {
dirltr() {
return this.$root.htmldir === 'ltr';
}
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.getReminderRules();
},
getReminderRules() {
axios.get('settings/personalization/reminderrules')
.then(response => {
this.reminderRules = response.data;
});
},
toggle(reminderRule, event) {
axios.post('settings/personalization/reminderrules/' + reminderRule.id)
.then(response => {
this.$notify({
group: 'main',
title: this.$t('settings.personalization_reminder_rule_save'),
text: '',
type: 'success'
});
this.$set(reminderRule, 'active', response.data.data.active);
});
}
}
};
</script>

View File

@@ -0,0 +1,108 @@
<template>
<div>
<!-- Timezone -->
<div class="form-group">
<form-select
:id="'timezone'"
:value="updatedTimezone"
:options="timezones"
:title="$t('settings.timezone')"
:required="true"
:iclass="'form-control'"
@input="timezoneUpdate"
/>
</div>
<!-- Reminders -->
<div class="form-group">
<form-select
:id="'reminder_time'"
:value="updatedReminder"
:options="hours"
:title="$t('settings.reminder_time_to_send')"
:required="true"
:iclass="'form-control'"
@input="reminderUpdate"
/>
<small class="form-text text-muted" v-html="message"></small>
</div>
</div>
</template>
<script>
export default {
props: {
timezone: {
type: String,
default: 'UTC',
},
timezones: {
type: Array,
default: function () {
return [];
}
},
reminder: {
type: String,
default: '',
},
hours: {
type: Array,
default: function () {
return [];
}
}
},
data() {
return {
message: '',
updatedTimezone: '',
updatedReminder: ''
};
},
mounted() {
this.prepareComponent();
},
methods: {
prepareComponent() {
this.updatedReminder = this.reminder;
this.updatedTimezone = this.timezone;
this.computeMessage();
},
timezoneUpdate: function(event) {
this.updatedTimezone = event;
this.computeMessage();
},
reminderUpdate: function(event) {
this.updatedReminder = event;
this.computeMessage();
},
computeMessage() {
var moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var now = moment();
var t = now.format('YYYY-MM-DD ' + this.updatedReminder + ':00');
var date = moment.tz(t, this.updatedTimezone);
if (date.isBefore(now)) {
date = date.add(1, 'days');
}
this.message = this.$t('settings.reminder_time_to_send_help', {
dateTime: date.format('LLL'),
dateTimeUtc: date.utc().format('YYYY-MM-DD HH:mm z')
});
}
}
};
</script>

View File

@@ -0,0 +1,297 @@
<template>
<form ref="form" class="mb4" :action="callback" method="post" @submit.prevent="subscribe()">
<notifications group="subscription" position="top middle" :duration="5000" width="400" />
<div class="form-group">
<div v-if="errors" role="alert" class="alert alert-danger w-100">
{{ errors }}
</div>
<div v-if="paymentSucceeded">
<h1 class="text-xl mt-2 mb-4 text-gray-700">
{{ $t('settings.subscriptions_payment_succeeded_title') }}
</h1>
<p v-if="successMessage" class="mb-6">
{{ successMessage }}
</p>
<p v-else class="mb-6">
{{ $t('settings.subscriptions_payment_succeeded') }}
</p>
</div>
<div v-else-if="paymentCancelled">
<h1 class="text-xl mt-2 mb-4 text-gray-700">
{{ $t('settings.subscriptions_payment_cancelled_title') }}
</h1>
<p class="mb-6">
{{ $t('settings.subscriptions_payment_cancelled') }}
</p>
</div>
<div v-else-if="! paymentProcessed" id="payment-elements" class="b--gray-monica ba pa4 br2 mb3 bg-black-05">
<div class="form-row">
<div class="mb3">
<form-input
:id="'cardholder-name'"
v-model="selectedName"
:input-type="'text'"
:iclass="'br3 b--black-30 ba pa3 w-100 f4'"
:required="true"
:title="$t('settings.subscriptions_upgrade_name')"
/>
</div>
<div class="mb3">
<form-input
:id="'address-zip'"
v-model="zip"
:input-type="'text'"
:iclass="'br3 b--black-30 ba pa3 w-100 f4'"
:title="$t('settings.subscriptions_upgrade_zip')"
/>
</div>
<label for="card-element">
{{ $t('settings.subscriptions_upgrade_credit') }}
</label>
<div id="card-element">
<!-- a Stripe Element will be inserted here. -->
</div>
</div>
<button
id="card-button"
class="btn btn-primary w-100 mt3"
:disabled="paymentProcessing"
@click.prevent="confirm ? confirmPayment() : subscribe()"
v-html="$t('settings.subscriptions_upgrade_submit', { amount: amount })"
>
</button>
</div>
<a v-if="paymentProcessed" :href="callback"
class="btn btn-secondary w-100 tc"
>
{{ $t('app.go_back') }}
</a>
</div>
<input type="hidden" name="_token" :value="token" />
<input type="hidden" name="plan" :value="plan" />
<input type="hidden" name="payment_method" :value="paymentMethod" />
</form>
</template>
<script>
export default {
props: {
name: {
type: String,
default: '',
},
stripeKey: {
type: String,
default: '',
},
clientSecret: {
type: String,
default: '',
},
plan: {
type: String,
default: '',
},
amount: {
type: String,
default: '',
},
callback: {
type: String,
default: '',
},
token: {
type: String,
default: '',
},
confirm: {
type: Boolean,
default: false,
},
paymentSucceeded: {
type: Boolean,
default: false,
},
paymentCancelled: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedName: '',
stripe: null,
zip: '',
errors: '',
successMessage: '',
cardElement: null,
paymentMethod: '',
paymentProcessing: false,
paymentProcessed: false,
};
},
watch: {
name() {
this.selectedName = this.name;
}
},
mounted() {
this.selectedName = this.name;
if (this.paymentSucceeded || this.paymentCancelled) {
this.paymentProcessed = true;
}
if (! this.paymentProcessed) {
this.start();
}
},
methods: {
start() {
this.stripe = Stripe(this.stripeKey);
const elements = this.stripe.elements();
// Custom styling can be passed to options when creating an Element.
// (Note that this demo uses a wider set of styles than the guide below.)
const style = {
base: {
color: '#32325d',
lineHeight: '18px',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
};
// Create an instance of the card Element
this.cardElement = elements.create('card', {
hidePostalCode: true,
style: style
});
// Add an instance of the card Element into the `card-element` <div>
this.cardElement.mount('#card-element');
// Handle real-time validation errors from the card Element.
var self = this;
this.cardElement.addEventListener('change', function(event) {
if (event.error) {
self.errors = event.error.message;
} else {
self.errors = '';
}
});
},
handleError(error) {
if (error.code === 'parameter_invalid_empty' &&
error.param === 'payment_method_data[billing_details][name]') {
this.errors = this.$t('settings.subscriptions_payment_error_name');
} else {
this.errors = error.message;
}
},
subscribe() {
var self = this;
this.errors = '';
this.paymentProcessing = true;
this.paymentProcessed = false;
this.stripe.handleCardSetup(
self.clientSecret,
self.cardElement,
{
payment_method_data: {
billing_details: {
name: self.selectedName,
address: {
postal_code: self.zip,
}
}
}
}
).then(function (result) {
self.paymentProcessing = false;
if (result.error) {
self.handleError(result.error);
} else {
// The card has been verified successfully...
self.paymentProcessed = true;
self.paymentSucceeded = true;
self.successMessage = self.$t('settings.subscriptions_payment_success');
self.notify(self.successMessage, true);
self.processPayment(result.setupIntent);
}
});
},
processPayment(setupIntent) {
var self = this;
this.paymentMethod = setupIntent.payment_method;
setTimeout(function () {
self.$refs.form.submit();
}, 10);
},
confirmPayment() {
var self = this;
this.paymentProcessing = true;
this.paymentProcessed = false;
this.errorMessage = '';
this.stripe.handleCardPayment(
self.clientSecret, self.cardElement, {
payment_method_data: {
billing_details: { name: this.selectedName }
}
}
).then(function (result) {
self.paymentProcessing = false;
if (result.error) {
self.handleError(result.error);
} else {
self.paymentProcessed = true;
self.paymentSucceeded = true;
self.successMessage = self.$t('settings.subscriptions_payment_success');
self.notify(self.successMessage, true);
setTimeout(function () {
window.location = self.callback;
}, 3000);
}
});
},
notify(text, success) {
this.$notify({
group: 'subscription',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

View File

@@ -0,0 +1,402 @@
<style scoped>
.time {
color: gray;
}
</style>
<template>
<div class="form-group">
<notifications group="webauthn" position="top middle" :duration="5000" width="400" />
<div v-if="method === 'register-modal'">
<h3>{{ $t('settings.webauthn_title') }}</h3>
<div v-if="currentkeys !== null">
<ul class="table">
<li v-for="key in currentkeys"
:key="key.id"
class="table-row"
>
<div class="table-cell w-30">
<strong>{{ key.name }}</strong>
</div>
<div class="table-cell time w-50">
<template v-if="key.counter > 0">
{{ $t('settings.webauthn_last_use', {timestamp: formatTime(key.updated_at)}) }}
</template>
</div>
<div class="table-cell actions">
<a class="pointer" href="" @click.prevent="showDeleteModal(key.id)">
{{ $t('app.delete') }}
</a>
</div>
</li>
</ul>
</div>
<slot></slot>
<a v-if="isSupported" class="btn btn-primary" href="" @click.prevent="showRegisterModal">
{{ $t('settings.webauthn_enable_description') }}
</a>
<small v-else>
{{ notSupportedMessage() }}
</small>
<sweet-modal
id="registerModal"
ref="registerModal"
overlay-theme="dark"
:title="$t('settings.webauthn_title')"
>
<div v-if="registerTab === '1'">
<p>
{{ $t('settings.webauthn_key_name_help') }}
</p>
<form-input
:id="'keyName'"
v-model="keyName"
:title="$t('settings.webauthn_key_name')"
:value="keyName"
:input-type="'text'"
:width="150"
:required="true"
@keyup.enter="showRegisterModalTab('2');startRegister();"
/>
</div>
<div v-if="registerTab === '2'">
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
<p>
<a href="" @click.prevent="startRegister()">
{{ $t('app.retry') }}
</a>
</p>
</div>
</div>
<div v-if="infoMessage !== ''" class="form-information-message mb3">
<div class="pa2">
<p class="mb0">
{{ infoMessage }}
</p>
</div>
</div>
<div v-if="errorMessage === ''" class="tc">
<img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png"
:alt="$t('settings.webauthn_insertKey')"
/>
</div>
<div v-if="errorMessage === ''" class="pa2">
<p>
{{ $t('settings.webauthn_insertKey') }}
</p>
<p>
{{ $t('settings.webauthn_buttonAdvise') }}
<br />
{{ $t('settings.webauthn_noButtonAdvise') }}
</p>
</div>
</div>
<div slot="button">
<a v-if="registerTab === '1'" class="btn" href="" @click.prevent="showRegisterModalTab('2');startRegister();">
{{ $t('pagination.next') }}
</a>
<a v-else class="btn" href="" @click.prevent="showRegisterModalTab('1')">
{{ $t('pagination.previous') }}
</a>
<a class="btn" href="" @click.prevent="closeRegisterModal()">
{{ $t('app.cancel') }}
</a>
</div>
</sweet-modal>
</div>
<div v-else>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
<p>
<a href="" @click.prevent="start()">
{{ $t('app.retry') }}
</a>
</p>
</div>
</div>
<div v-if="infoMessage !== ''" class="form-information-message mb3">
<div class="pa2">
<p class="mb0">
{{ infoMessage }}
</p>
</div>
</div>
<div class="tc">
<img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png"
:alt="$t('settings.webauthn_insertKey')"
/>
</div>
<div class="pa2">
<p>
{{ $t('settings.webauthn_insertKey') }}
</p>
<p>
{{ $t('settings.webauthn_buttonAdvise') }}
<br />
{{ $t('settings.webauthn_noButtonAdvise') }}
</p>
</div>
</div>
<sweet-modal ref="delete" overlay-theme="dark" title="Remove a key">
<form>
<div class="mb4">
{{ $t('settings.webauthn_delete_confirmation') }}
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn" href="" @click.prevent="webauthnRemove(keyToTrash)">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
import * as WebAuthn from '../../../../vendor/asbiin/laravel-webauthn/resources/js/webauthn.js';
export default {
components: {
SweetModal
},
props: {
keys: {
type: Array,
default: function () {
return [];
}
},
publicKey: {
type: Object,
default: null,
},
method: {
type: String,
default: '',
},
timezone: {
type: String,
default: '',
},
script: {
type: String,
default: '',
},
},
data() {
return {
isSupported: true,
errorMessage: '',
infoMessage: '',
success: false,
currentkeys: [],
keyToTrash: '',
keyName: '',
registerTab: '',
data: null,
webauthn: null,
};
},
mounted() {
this.prepareComponent();
this.start();
},
methods: {
prepareComponent() {
this.currentkeys = this.keys;
this.data = this.registerdata;
this.webauthn = new WebAuthn((name, message) => {
this.errorMessage = this._errorMessage(name, message);
});
},
_errorMessage(name, message) {
switch (name) {
case 'InvalidStateError':
return this.$t('settings.webauthn_error_already_used');
case 'NotAllowedError':
return this.$t('settings.webauthn_error_not_allowed');
default:
return message;
}
},
notSupportedMessage() {
return this.$t('settings.webauthn_'+this.webauthn.notSupportedMessage());
},
start() {
var self = this;
this.errorMessage = '';
if (! this.webauthn.webAuthnSupport()) {
this.isSupported = false;
this.errorMessage = this.notSupportedMessage();
}
switch(this.method) {
case 'register':
setTimeout(function () {
self.webauthn.register(
self.publicKey,
function (datas) { self.webauthnRegisterCallback(datas, true); }
);
}, 10);
break;
case 'login':
this.webauthn.sign(
this.publicKey,
function (data) { self.webauthnLoginCallback(data); }
);
break;
}
},
showRegisterModal() {
this.errorMessage = '';
this.infoMessage = '';
this.keyName = '';
this.success = false;
this.showRegisterModalTab('1');
this.$refs.registerModal.open();
},
showRegisterModalTab(tab) {
this.registerTab = tab;
},
startRegister() {
var self = this;
this.errorMessage = '';
axios.post('webauthn/keys/options')
.then(response => {
if (self.registerTab === '2') {
var data = response.data.publicKey;
setTimeout(function () {
self.webauthn.register(
data,
function (datas) { self.webauthnRegisterCallback(datas, false); }
);
}, 10);
}
}).catch(error => {
self.notify(error.response.data.message, false);
});
},
closeRegisterModal() {
this.$refs.registerModal.close();
this.showRegisterModalTab('');
},
webauthnRegisterCallback(data, redirect) {
var self = this;
axios.post('webauthn/keys', {
...data,
name: self.keyName,
}).then(response => {
self.success = true;
self.notify(self.$t('settings.webauthn_success'), true);
self.currentkeys.push({
id: response.result.id,
name: response.result.name,
});
}).then(response => {
if (redirect) {
setTimeout(function () {
window.location = response.data.callback;
}, 100);
} else {
self.closeRegisterModal();
}
}).catch(error => {
self.errorMessage = error.message ? error.message : error.response.data.message;
});
},
webauthnLoginCallback(data) {
var self = this;
axios.post('webauthn/auth', {
...data
}).then(response => {
self.success = true;
self.notify(self.$t('settings.webauthn_success'), true);
window.location = response.data.callback;
}).catch(error => {
self.errorMessage = error.message ? error.message : error.response.data.message;
});
},
webauthnRemove(id) {
var self = this;
axios.delete('webauthn/keys/'+id)
.then(response => {
self.currentkeys.splice(self.currentkeys.indexOf(self.currentkeys.find(item => item.id === response.data.id)), 1);
self.success = true;
self.notify(self.$t('settings.webauthn_delete_success'), true);
self.closeDeleteModal();
}).catch(error => {
self.errorMessage = error.response.data.message;
});
},
showDeleteModal(id) {
this.keyToTrash = id;
this.$refs.delete.open();
},
closeDeleteModal() {
this.$refs.delete.close();
},
formatTime(value) {
var moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var t = moment(value);
var date = moment.tz(t, this.timezone);
return date.format('LLLL');
},
notify(text, success) {
this.$notify({
group: 'webauthn',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>

23
resources/js/methods.js vendored Normal file
View File

@@ -0,0 +1,23 @@
export default {
/**
* Update the default tab view.
*
* @param {string} view
*/
updateDefaultProfileView(view) {
axios.post('settings/updateDefaultProfileView', { name: view })
.then(response => {
this.global_profile_default_view = view;
});
},
/**
* Fix avatar in case img is on error.
*
* @param {event} event
*/
fixAvatarDisplay(event) {
event.srcElement.classList = ['hidden'];
event.srcElement.nextElementSibling.classList.remove('hidden');
}
};

53
resources/js/pluralization.js vendored Normal file
View File

@@ -0,0 +1,53 @@
/**
* Pluralization form for every langage not following engl-ish like form.
*
* 'plurals' functions represent possible plural form transformations.
* This return a list of lang/plural function to apply.
*
* @see https://github.com/laravel/framework/blob/master/src/Illuminate/Translation/MessageSelector.php
*/
function pluralA (choice, choicesLength) {
return 0;
}
function pluralB (choice, choicesLength) {
let number = Math.abs(choice);
number = ((number == 0) || (number == 1)) ? 0 : 1;
return Math.min(number, choicesLength - 1);
}
function pluralC (choice, choicesLength) {
let number = Math.abs(choice);
number = (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
return Math.min(number, choicesLength - 1);
}
function pluralD (choice, choicesLength) {
let number = Math.abs(choice);
number = ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
return Math.min(number, choicesLength - 1);
}
function pluralE (choice, choicesLength) {
let number = Math.abs(choice);
number = (number == 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5))));
return Math.min(number, choicesLength - 1);
}
function pluralF (choice, choicesLength) {
let number = Math.abs(choice);
number = (number == 1) ? 0 : ((number == 2) ? 1 : (number < 10 && number % 10 == 0) ? 2 : 3);
return Math.min(number, choicesLength - 1);
}
export default {
'ar': pluralE,
'cs': pluralC,
'fr': pluralB,
'he': pluralF,
'hr': pluralD,
'id': pluralA,
'ja': pluralA,
'ru': pluralD,
'tr': pluralA,
'uk': pluralD,
'vi': pluralA,
'zh': pluralA,
'zh-TW': pluralA,
};

53
resources/js/stripe.js vendored Normal file
View File

@@ -0,0 +1,53 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
import Vue from 'vue';
window.Vue = Vue;
// Notifications
import Notifications from 'vue-notification';
Vue.use(Notifications);
// Custom components
Vue.component(
'StripeSubscription',
require('./components/settings/Subscription.vue').default
);
// Form elements
Vue.component(
'FormInput',
require('./components/partials/form/Input.vue').default
);
Vue.component(
'ContactSearch',
require('./components/people/ContactSearch.vue').default
);
var common = require('./common').default;
common.loadLanguage(window.Laravel.locale, true).then((i18n) => {
// the Vue appplication
const app = new Vue({
i18n,
data: {
htmldir: window.Laravel.htmldir,
locale: i18n.locale,
},
}).$mount('#app');
return app;
});

21
resources/js/testing.js vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* Add cy-name and cy-items directives.
* These are only active on local or testing environment.
*/
function testingDirective(el, binding, vnode) {
if (window.Laravel.env != 'production') {
var value = '';
try {
value = function(expr) {
return eval(expr);
}.call(vnode.context, ' with(this) { ' + binding.expression + ' } ');
} catch (e) {
value = binding.value;
}
el.setAttribute(binding.name, value.toString());
}
}
Vue.directive('cy-items', testingDirective);
Vue.directive('cy-name', testingDirective);

7
resources/lang/ar.json Normal file
View File

@@ -0,0 +1,7 @@
{
"The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute يجب أن يحتوي على الأقل حرف كبير واحد وحرف صغير واحد.",
"The :attribute must contain at least one letter.": ":attribute يجب أن يحتوي على الأقل حرف واحد.",
"The :attribute must contain at least one symbol.": ":attribute يجب أن يحتوي على الأقل رمز واحد.",
"The :attribute must contain at least one number.": ":attribute يجب أن يحتوي على الأقل رقم واحد.",
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute المستخدم ضمن قائمة البيانات المسربة. الرجاء اختيار :attribute مختلف."
}

571
resources/lang/ar/app.php Normal file
View File

@@ -0,0 +1,571 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'yes' => 'Yes',
'no' => 'No',
'update' => 'تحديث',
'save' => 'حفظ',
'add' => 'إضافة',
'cancel' => 'إلغاء',
'confirm' => 'Confirm',
'delete_confirm' => 'Are you sure?',
'delete' => 'حذف',
'edit' => 'تعديل',
'upload' => 'رفع',
'download' => 'Download',
'save_close' => 'Save and close',
'close' => 'إغلاق',
'copy' => 'Copy',
'create' => 'إنشاء',
'remove' => 'إزالة',
'revoke' => 'إلغاء',
'done' => 'تم',
'back' => 'Back',
'verify' => 'تحقق',
'new' => 'جديد',
'unknown' => 'لا أعرف',
'load_more' => 'تحميل المزيد',
'loading' => 'Loading…',
'with' => 'مع',
'today' => 'اليوم',
'yesterday' => 'أمس',
'another_day' => 'يوم آخر',
'date' => 'التاريخ',
'type' => 'النوع',
'zoom' => 'Zoom',
'upgrade' => 'Upgrade to unlock',
'percent_uploaded' => '{percent}% uploaded',
'retry' => 'Retry',
'filter' => 'Filter the list',
'go_back' => 'Go back',
'file_selected' => 'One file selected…|{count} files selected…',
'application_title' => 'Monica (مونيكا) – مدير العلاقات الشخصية',
'application_description' => 'Monica هو أداة لإدارة تفاعلاتك مع أحبائك، أصدقائك و عائلتك.',
'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.',
'markdown_description' => 'هل تريد تنسيق النص الخاص بك بطريقة لطيفة؟ نحن ندعم التحديد و إضافة السماكة، و الإمالة و القوائم و المزيد.',
'markdown_link' => 'اقرأ الوثائق',
'header_settings_link' => 'الإعدادات',
'header_logout_link' => 'تسجيل الخروج',
'header_changelog_link' => 'تغيرات المنتج',
'main_nav_cta' => 'إضافة أشخاص',
'main_nav_dashboard' => 'لوحة التحكم',
'main_nav_family' => 'جهات الاتصال',
'main_nav_journal' => 'يوميات',
'main_nav_activities' => 'الأنشطة',
'main_nav_tasks' => 'المهام',
'footer_remarks' => 'Comments?',
'footer_send_email' => 'Send us an email',
'footer_privacy' => 'سياسة الخصوصية',
'footer_release' => 'ملاحظات الإصدار',
'footer_newsletter' => 'النشرة الإخبارية',
'footer_source_code' => 'ساهم',
'footer_version' => 'الإصدار: :version',
'footer_new_version' => 'A new version of Monica is available',
'footer_modal_version_whats_new' => 'ما الجديد؟',
'footer_modal_version_release_away' => 'إن إصدارك أقدم من آخر إصدار متاح. يجب أن تقوم بالتحديث. | أنت متأخر بـ:number إصدارات من آخر إصدار متاح. يجب أن تقوم بالتحديث.',
'breadcrumb_dashboard' => 'لوحة التحكم',
'breadcrumb_list_contacts' => 'قائمة الأشخاص',
'breadcrumb_archived_contacts' => 'Archived contacts',
'breadcrumb_journal' => 'يوميات',
'breadcrumb_settings' => 'الإعدادات',
'breadcrumb_settings_export' => 'استخراج',
'breadcrumb_settings_users' => 'المستخدمون',
'breadcrumb_settings_users_add' => 'إضافة مستخدم',
'breadcrumb_settings_subscriptions' => 'اشتراك',
'breadcrumb_settings_import' => 'استيراد',
'breadcrumb_settings_import_report' => 'استيراد التقرير',
'breadcrumb_settings_import_upload' => 'رفع',
'breadcrumb_settings_tags' => 'العلامات',
'breadcrumb_add_significant_other' => 'Add significant other',
'breadcrumb_edit_significant_other' => 'Edit significant other',
'breadcrumb_add_note' => 'أضف ملاحظة',
'breadcrumb_edit_note' => 'حرر ملاحظة',
'breadcrumb_api' => 'API (واجهة برمجة التطبيق)',
'breadcrumb_dav' => 'DAV Resources',
'breadcrumb_edit_introductions' => 'كيف تقابلتما',
'breadcrumb_settings_personalization' => 'التخصيص',
'breadcrumb_settings_security' => 'الأمن',
'breadcrumb_settings_security_2fa' => 'المصادقة الثنائية',
'breadcrumb_profile' => 'الملف الشخصي لـ :name',
'gender_male' => 'رجل',
'gender_female' => 'إمرأة',
'gender_none' => 'أُفَضل ألا أقول',
'gender_no_gender' => 'No gender',
'error_title' => 'عفواً! حصل خطأ ما.',
'error_unauthorized' => 'ليس لديك الصلاحية لتحرير هذا المصدر.',
'error_user_account' => 'This user does not belong to the given account.',
'error_save' => 'لقد حصل خطأ بينما كنا نحاول حفظ البيانات.',
'error_try_again' => 'حدث خطأ ما. الرجاء المحاولة مرة أخرى.',
'error_id' => 'معرف الخطأ: :id',
'error_unavailable' => 'Service unavailable',
'error_maintenance' => 'Maintenance in progress. We’ll be right back.',
'error_help' => 'سنعود إليك حالاً.',
'error_twitter' => 'تابع <a href="https://twitter.com/:twitter">حساب تويتر</a> الخاص بنا، ليتم تنبيهك عندما يعود الموقع مجدداً.',
'error_no_term' => 'There is no policy for this instance yet.',
'default_save_success' => 'تم حفظ البيانات.',
'compliance_title' => 'نعتذر للإزعاج.',
'compliance_desc' => 'لقد قمنا بتغيير <a href=":urlterm" hreflang=":hreflang">شروط الإستخدام</a>و<a href=":url" hreflang=":hreflang">سياسة الخصوصية</a> الخاصة بنا. بموجب القانون، نطلب منك مراجعتها و قبولها لكي يمكنك الإستمرار في استخدام حسابك.',
'compliance_desc_end' => 'نحن لن نسيء إلى بياناتك أو حسابك و لن نفعل ذلك أبداً.',
'compliance_terms' => 'القبول بالشروط و سياسة الخصوصية الجديدية',
// Relationship types
// Yes, each relationship type has 8 strings associated with it.
// This is because we need to indicate the name of the relationship type,
// and also the name of the opposite side of this relationship (father/son),
// and then, the feminine version of the string. Finally, in some sentences
// in the UI, we need to include the name of the person we add the relationship
// to.
'relationship_type_group_love' => 'علاقات حب',
'relationship_type_group_family' => 'علاقات عائلية',
'relationship_type_group_friend' => 'علاقات صداقة',
'relationship_type_group_work' => 'علاقات عمل',
'relationship_type_group_other' => 'نوع آخر من العلاقات',
'relationship_type_partner' => 'significant other',
'relationship_type_partner_female' => 'significant other',
'relationship_type_partner_male' => 'significant other',
'relationship_type_partner_with_name' => ':name’s significant other',
'relationship_type_partner_female_with_name' => ':name’s significant other',
'relationship_type_partner_male_with_name' => ':name’s significant other',
'relationship_type_spouse' => 'زوج',
'relationship_type_spouse_female' => 'wife',
'relationship_type_spouse_male' => 'husband',
'relationship_type_spouse_with_name' => 'زوج :name’s',
'relationship_type_spouse_female_with_name' => ':name’s wife',
'relationship_type_spouse_male_with_name' => ':name’s husband',
'relationship_type_date' => 'موعد',
'relationship_type_date_female' => 'موعد',
'relationship_type_date_male' => 'date',
'relationship_type_date_with_name' => ':name’s date',
'relationship_type_date_female_with_name' => ':name’s date',
'relationship_type_date_male_with_name' => ':name’s date',
'relationship_type_lover' => 'عاشق',
'relationship_type_lover_female' => 'عاشقة',
'relationship_type_lover_male' => 'lover',
'relationship_type_lover_with_name' => ':name’s lover',
'relationship_type_lover_female_with_name' => ':name’s lover',
'relationship_type_lover_male_with_name' => ':name’s lover',
'relationship_type_inlovewith' => 'مغرم بـ',
'relationship_type_inlovewith_female' => 'مغرمة بـ',
'relationship_type_inlovewith_male' => 'in love with',
'relationship_type_inlovewith_with_name' => 'someone :name is in love with',
'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with',
'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with',
'relationship_type_lovedby' => 'محبوب من قِبل',
'relationship_type_lovedby_female' => 'محبوبة من قِبل',
'relationship_type_lovedby_male' => 'loved by',
'relationship_type_lovedby_with_name' => ':name’s secret lover',
'relationship_type_lovedby_female_with_name' => ':name’s secret lover',
'relationship_type_lovedby_male_with_name' => ':name’s secret lover',
'relationship_type_ex' => 'ex-partner',
'relationship_type_ex_female' => 'ex-girlfriend',
'relationship_type_ex_male' => 'ex-boyfriend',
'relationship_type_ex_with_name' => ':name’s ex-partner',
'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend',
'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend',
'relationship_type_parent' => 'parent',
'relationship_type_parent_female' => 'أم',
'relationship_type_parent_male' => 'father',
'relationship_type_parent_with_name' => ':name’s parent',
'relationship_type_parent_female_with_name' => 'والدة :name',
'relationship_type_parent_male_with_name' => ':name’s father',
'relationship_type_child' => 'child',
'relationship_type_child_female' => 'ابنة',
'relationship_type_child_male' => 'son',
'relationship_type_child_with_name' => ':name’s child',
'relationship_type_child_female_with_name' => 'ابنة :name',
'relationship_type_child_male_with_name' => ':name’s son',
'relationship_type_stepparent' => 'step-parent',
'relationship_type_stepparent_female' => 'stepmother',
'relationship_type_stepparent_male' => 'stepfather',
'relationship_type_stepparent_with_name' => ':name’s step-parent',
'relationship_type_stepparent_female_with_name' => ':name’s stepmother',
'relationship_type_stepparent_male_with_name' => ':name’s stepfather',
'relationship_type_stepchild' => 'stepchild',
'relationship_type_stepchild_female' => 'stepdaughter',
'relationship_type_stepchild_male' => 'stepson',
'relationship_type_stepchild_with_name' => ':name’s stepchild',
'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter',
'relationship_type_stepchild_male_with_name' => ':name’s stepson',
'relationship_type_sibling' => 'sibling',
'relationship_type_sibling_female' => 'أخت',
'relationship_type_sibling_male' => 'brother',
'relationship_type_sibling_with_name' => ':name’s sibling',
'relationship_type_sibling_female_with_name' => 'شقيقة :name',
'relationship_type_sibling_male_with_name' => ':name’s brother',
'relationship_type_grandparent' => 'grandparent',
'relationship_type_grandparent_female' => 'grandmother',
'relationship_type_grandparent_male' => 'grandfather',
'relationship_type_grandparent_with_name' => ':name’s grandparent',
'relationship_type_grandparent_female_with_name' => ':name’s grandmother',
'relationship_type_grandparent_male_with_name' => ':name’s grandfather',
'relationship_type_grandchild' => 'grandchild',
'relationship_type_grandchild_female' => 'granddaughter',
'relationship_type_grandchild_male' => 'grandson',
'relationship_type_grandchild_with_name' => ':name’s grandchild',
'relationship_type_grandchild_female_with_name' => ':name’s granddauther',
'relationship_type_grandchild_male_with_name' => ':name’s grandson',
'relationship_type_uncle' => 'عم/خال',
'relationship_type_uncle_female' => 'عمة/خالة',
'relationship_type_uncle_male' => 'uncle',
'relationship_type_uncle_with_name' => 'عم/خال :name',
'relationship_type_uncle_female_with_name' => 'عمة/خالة :name',
'relationship_type_uncle_male_with_name' => ':name’s uncle',
'relationship_type_nephew' => 'ابن شقيق',
'relationship_type_nephew_female' => 'ابنة شقيقة',
'relationship_type_nephew_male' => 'nephew',
'relationship_type_nephew_with_name' => 'ابن شقيق :name',
'relationship_type_nephew_female_with_name' => 'ابنة شقيقة :name',
'relationship_type_nephew_male_with_name' => ':name’s nephew',
'relationship_type_cousin' => 'ابن عم/خال',
'relationship_type_cousin_female' => 'ابنة عمة/خالة',
'relationship_type_cousin_male' => 'cousin',
'relationship_type_cousin_with_name' => 'ابن عم/خال :name',
'relationship_type_cousin_female_with_name' => 'ابنة عمة/خالة :name',
'relationship_type_cousin_male_with_name' => ':name’s cousin',
'relationship_type_godfather' => 'godparent',
'relationship_type_godfather_female' => 'godmother',
'relationship_type_godfather_male' => 'godfather',
'relationship_type_godfather_with_name' => ':name’s godparent',
'relationship_type_godfather_female_with_name' => ':name’s godmother',
'relationship_type_godfather_male_with_name' => ':name’s godfather',
'relationship_type_godson' => 'godchild',
'relationship_type_godson_female' => 'goddaughter',
'relationship_type_godson_male' => 'godson',
'relationship_type_godson_with_name' => ':name’s godchild',
'relationship_type_godson_female_with_name' => ':name’s goddaughter',
'relationship_type_godson_male_with_name' => ':name’s godson',
'relationship_type_friend' => 'صديق',
'relationship_type_friend_female' => 'صديقة',
'relationship_type_friend_male' => 'friend',
'relationship_type_friend_with_name' => 'صديق :name',
'relationship_type_friend_female_with_name' => 'صديقة :name',
'relationship_type_friend_male_with_name' => ':name’s friend',
'relationship_type_bestfriend' => 'الصديق المفضل',
'relationship_type_bestfriend_female' => 'الصديقة المفضلة',
'relationship_type_bestfriend_male' => 'best friend',
'relationship_type_bestfriend_with_name' => 'صديق :name المفضل',
'relationship_type_bestfriend_female_with_name' => 'صديقة :name المفضلة',
'relationship_type_bestfriend_male_with_name' => ':name’s best friend',
'relationship_type_colleague' => 'زميل',
'relationship_type_colleague_female' => 'زميلة',
'relationship_type_colleague_male' => 'colleague',
'relationship_type_colleague_with_name' => 'زميل :name',
'relationship_type_colleague_female_with_name' => 'زميلة :name',
'relationship_type_colleague_male_with_name' => ':name’s colleague',
'relationship_type_boss' => 'مدير',
'relationship_type_boss_female' => 'مديرة',
'relationship_type_boss_male' => 'boss',
'relationship_type_boss_with_name' => 'مدير :name',
'relationship_type_boss_female_with_name' => 'مديرة :name',
'relationship_type_boss_male_with_name' => ':name’s boss',
'relationship_type_subordinate' => 'مرؤوس',
'relationship_type_subordinate_female' => 'مرؤوسة',
'relationship_type_subordinate_male' => 'subordinate',
'relationship_type_subordinate_with_name' => 'مرؤوس :name',
'relationship_type_subordinate_female_with_name' => 'مرؤوسة :name',
'relationship_type_subordinate_male_with_name' => ':name’s subordinate',
'relationship_type_mentor' => 'مرشد',
'relationship_type_mentor_female' => 'مرشدة',
'relationship_type_mentor_male' => 'mentor',
'relationship_type_mentor_with_name' => 'مرشد :name',
'relationship_type_mentor_female_with_name' => 'مرشدة :name',
'relationship_type_mentor_male_with_name' => ':name’s mentor',
'relationship_type_protege' => 'protégé',
'relationship_type_protege_female' => 'protégé',
'relationship_type_protege_male' => 'protégé',
'relationship_type_protege_with_name' => ':name’s protégé',
'relationship_type_protege_female_with_name' => ':name’s protégé',
'relationship_type_protege_male_with_name' => ':name’s protégé',
'relationship_type_ex_husband' => 'ex-spouse',
'relationship_type_ex_husband_female' => 'طليقة',
'relationship_type_ex_husband_male' => 'ex-husband',
'relationship_type_ex_husband_with_name' => ':name’s ex-spouse',
'relationship_type_ex_husband_female_with_name' => 'زوجة :name السابقة',
'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband',
// emotions
'emotion_primary_love' => 'Love',
'emotion_primary_joy' => 'Joy',
'emotion_primary_surprise' => 'Surprise',
'emotion_primary_anger' => 'Anger',
'emotion_primary_sadness' => 'Sadness',
'emotion_primary_fear' => 'Fear',
'emotion_secondary_affection' => 'Affection',
'emotion_secondary_lust' => 'Lust',
'emotion_secondary_longing' => 'Longing',
'emotion_secondary_cheerfulness' => 'Cheerfulness',
'emotion_secondary_zest' => 'Zest',
'emotion_secondary_contentment' => 'Contentment',
'emotion_secondary_pride' => 'Pride',
'emotion_secondary_optimism' => 'Optimism',
'emotion_secondary_enthrallment' => 'Enthrallment',
'emotion_secondary_relief' => 'Relief',
'emotion_secondary_surprise' => 'Surprise',
'emotion_secondary_irritation' => 'Irritation',
'emotion_secondary_exasperation' => 'Exasperation',
'emotion_secondary_rage' => 'Rage',
'emotion_secondary_disgust' => 'Disgust',
'emotion_secondary_envy' => 'Envy',
'emotion_secondary_suffering' => 'Suffering',
'emotion_secondary_sadness' => 'Sadness',
'emotion_secondary_disappointment' => 'Disappointment',
'emotion_secondary_shame' => 'Shame',
'emotion_secondary_neglect' => 'Neglect',
'emotion_secondary_sympathy' => 'Sympathy',
'emotion_secondary_horror' => 'Horror',
'emotion_secondary_nervousness' => 'Nervousness',
'emotion_adoration' => 'Adoration',
'emotion_affection' => 'Affection',
'emotion_love' => 'Love',
'emotion_fondness' => 'Fondness',
'emotion_liking' => 'Liking',
'emotion_attraction' => 'Attraction',
'emotion_caring' => 'Caring',
'emotion_tenderness' => 'Tenderness',
'emotion_compassion' => 'Compassion',
'emotion_sentimentality' => 'Sentimentality',
'emotion_arousal' => 'Arousal',
'emotion_desire' => 'Desire',
'emotion_lust' => 'Lust',
'emotion_passion' => 'Passion',
'emotion_infatuation' => 'Infatuation',
'emotion_longing' => 'Longing',
'emotion_amusement' => 'Amusement',
'emotion_bliss' => 'Bliss',
'emotion_cheerfulness' => 'Cheerfulness',
'emotion_gaiety' => 'Gaiety',
'emotion_glee' => 'Glee',
'emotion_jolliness' => 'Jolliness',
'emotion_joviality' => 'Joviality',
'emotion_joy' => 'Joy',
'emotion_delight' => 'Delight',
'emotion_enjoyment' => 'Enjoyment',
'emotion_gladness' => 'Gladness',
'emotion_happiness' => 'Happiness',
'emotion_jubilation' => 'Jubilation',
'emotion_elation' => 'Elation',
'emotion_satisfaction' => 'Satisfaction',
'emotion_ecstasy' => 'Ecstasy',
'emotion_euphoria' => 'Euphoria',
'emotion_enthusiasm' => 'Enthusiasm',
'emotion_zeal' => 'Zeal',
'emotion_zest' => 'Zest',
'emotion_excitement' => 'Excitement',
'emotion_thrill' => 'Thrill',
'emotion_exhilaration' => 'Exhilaration',
'emotion_contentment' => 'Contentment',
'emotion_pleasure' => 'Pleasure',
'emotion_pride' => 'Pride',
'emotion_eagerness' => 'Eagerness',
'emotion_hope' => 'Hope',
'emotion_optimism' => 'Optimism',
'emotion_enthrallment' => 'Enthrallment',
'emotion_rapture' => 'Rapture',
'emotion_relief' => 'Relief',
'emotion_amazement' => 'Amazement',
'emotion_surprise' => 'Surprise',
'emotion_astonishment' => 'Astonishment',
'emotion_aggravation' => 'Aggravation',
'emotion_irritation' => 'Irritation',
'emotion_agitation' => 'Agitation',
'emotion_annoyance' => 'Annoyance',
'emotion_grouchiness' => 'Grouchiness',
'emotion_grumpiness' => 'Grumpiness',
'emotion_exasperation' => 'Exasperation',
'emotion_frustration' => 'Frustration',
'emotion_anger' => 'Anger',
'emotion_rage' => 'Rage',
'emotion_outrage' => 'Outrage',
'emotion_fury' => 'Fury',
'emotion_wrath' => 'Wrath',
'emotion_hostility' => 'Hostility',
'emotion_ferocity' => 'Ferocity',
'emotion_bitterness' => 'Bitterness',
'emotion_hate' => 'Hate',
'emotion_loathing' => 'Loathing',
'emotion_scorn' => 'Scorn',
'emotion_spite' => 'Spite',
'emotion_vengefulness' => 'Vengefulness',
'emotion_dislike' => 'Dislike',
'emotion_resentment' => 'Resentment',
'emotion_disgust' => 'Disgust',
'emotion_revulsion' => 'Revulsion',
'emotion_contempt' => 'Contempt',
'emotion_envy' => 'Envy',
'emotion_jealousy' => 'Jealousy',
'emotion_agony' => 'Agony',
'emotion_suffering' => 'Suffering',
'emotion_hurt' => 'Hurt',
'emotion_anguish' => 'Anguish',
'emotion_depression' => 'Depression',
'emotion_despair' => 'Despair',
'emotion_hopelessness' => 'Hopelessness',
'emotion_gloom' => 'Gloom',
'emotion_glumness' => 'Glumness',
'emotion_sadness' => 'Sadness',
'emotion_unhappiness' => 'Unhappiness',
'emotion_grief' => 'Grief',
'emotion_sorrow' => 'Sorrow',
'emotion_woe' => 'Woe',
'emotion_misery' => 'Misery',
'emotion_melancholy' => 'Melancholy',
'emotion_dismay' => 'Dismay',
'emotion_disappointment' => 'Disappointment',
'emotion_displeasure' => 'Displeasure',
'emotion_guilt' => 'Guilt',
'emotion_shame' => 'Shame',
'emotion_regret' => 'Regret',
'emotion_remorse' => 'Remorse',
'emotion_alienation' => 'Alienation',
'emotion_isolation' => 'Isolation',
'emotion_neglect' => 'Neglect',
'emotion_loneliness' => 'Loneliness',
'emotion_rejection' => 'Rejection',
'emotion_homesickness' => 'Homesickness',
'emotion_defeat' => 'Defeat',
'emotion_dejection' => 'Dejection',
'emotion_insecurity' => 'Insecurity',
'emotion_embarrassment' => 'Embarrassment',
'emotion_humiliation' => 'Humiliation',
'emotion_insult' => 'Insult',
'emotion_pity' => 'Pity',
'emotion_sympathy' => 'Sympathy',
'emotion_alarm' => 'Alarm',
'emotion_shock' => 'Shock',
'emotion_fear' => 'Fear',
'emotion_fright' => 'Fright',
'emotion_horror' => 'Horror',
'emotion_terror' => 'Terror',
'emotion_panic' => 'Panic',
'emotion_hysteria' => 'Hysteria',
'emotion_mortification' => 'Mortification',
'emotion_anxiety' => 'Anxiety',
'emotion_nervousness' => 'Nervousness',
'emotion_tenseness' => 'Tenseness',
'emotion_uneasiness' => 'Uneasiness',
'emotion_apprehension' => 'Apprehension',
'emotion_worry' => 'Worry',
'emotion_distress' => 'Distress',
'emotion_dread' => 'Dread',
// weather
'weather_sunny' => 'Sunny',
'weather_clear' => 'Clear',
'weather_clear-day' => 'Clear',
'weather_clear-night' => 'Clear night',
'weather_light-drizzle' => 'Light drizzle',
'weather_patchy-light-drizzle' => 'Patchy light drizzle',
'weather_patchy-light-rain' => 'Patchy light rain',
'weather_light-rain' => 'Light rain',
'weather_moderate-rain-at-times' => 'Moderate rain at times',
'weather_moderate-rain' => 'Moderate rain',
'weather_patchy-rain-possible' => 'Patchy rain possible',
'weather_heavy-rain-at-times' => 'Heavy rain at times',
'weather_heavy-rain' => 'Heavy rain',
'weather_light-freezing-rain' => 'Light freezing rain',
'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain',
'weather_light-sleet' => 'Light sleet',
'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower',
'weather_light-rain-shower' => 'Light rain shower',
'weather_torrential-rain-shower' => 'Torrential rain shower',
'weather_rain' => 'Rain',
'weather_snow' => 'Snow',
'weather_blowing-snow' => 'Blowing snow',
'weather_patchy-light-snow' => 'Patchy light snow',
'weather_light-snow' => 'Light snow',
'weather_patchy-moderate-snow' => 'Patchy moderate snow',
'weather_moderate-snow' => 'Moderate snow',
'weather_patchy-heavy-snow' => 'Patchy heavy snow',
'weather_heavy-snow' => 'Heavy snow',
'weather_light-snow-showers' => 'Light snow showers',
'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers',
'weather_patchy-snow-possible' => 'Patchy snow possible',
'weather_patchy-sleet-possible' => 'Patchy sleet possible',
'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet',
'weather_light-sleet-showers' => 'Light sleet showers',
'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers',
'weather_sleet' => 'Sleet',
'weather_wind' => 'Wind',
'weather_fog' => 'Fog',
'weather_freezing-fog' => 'Freezing fog',
'weather_mist' => 'Mist',
'weather_blizzard' => 'Blizzard',
'weather_overcast' => 'Overcast',
'weather_cloudy' => 'Cloudy',
'weather_partly-cloudy-day' => 'Partly cloudy',
'weather_partly-cloudy-night' => 'Partly cloudy',
'weather_freezing-drizzle' => 'Freezing drizzle',
'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle',
'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible',
'weather_ice-pellets' => 'Ice pellets',
'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets',
'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets',
'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible',
'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder',
'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder',
'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder',
'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder',
'weather_current_temperature_celsius' => ':temperature °C',
'weather_current_temperature_fahrenheit' => ':temperature °F',
'weather_current_title' => 'Current weather',
// dav
'dav_contacts' => 'Contacts',
'dav_contacts_description' => ':name’s contacts',
'dav_birthdays' => 'Birthdays',
'dav_birthdays_description' => ':name’s contact’s birthdays',
'dav_tasks' => 'Tasks',
'dav_tasks_description' => ':name’s tasks',
// contact list
'contact_list_avatar' => 'Avatar',
'contact_list_name' => 'Contact',
'contact_list_description' => 'Description',
];

View File

@@ -0,0 +1,89 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع البيانات المسجلة لدينا.',
'throttle' => 'عدد كبير جدا من محاولات الدخول. يرجى المحاولة مرة أخرى بعد :seconds ثانية.',
'not_authorized' => 'ليس لديك الصلاحية لتنفيذ هذا الأمر',
'signup_disabled' => 'تسجيل الإشتراك معطل حالياً',
'signup_error' => 'An error occured trying to register the user',
'back_homepage' => 'العودة إلى الصفحة الرئيسية',
'mfa_auth_otp' => 'المصادقة مع جهاز العامل الثنائي الخاص بك',
'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)',
'2fa_title' => 'المصادقة الثنائية',
'2fa_wrong_validation' => 'فشلت المصادقة الثنائية.',
'2fa_one_time_password' => 'رمز المصادقة الثنائية',
'2fa_recuperation_code' => 'أدخل رمز استرداد العامل الثنائي',
'2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code',
'2fa_otp_help' => 'قم بفتح تطبيق المصادقة الثنائية في هاتفك و انسخ الرمز',
'login_to_account' => 'تسجيل الدخول إلى حسابك',
'login_with_recovery' => 'Login with a recovery code',
'login_again' => 'الرجاء تسجيل الدخول مجدداً لحسابك',
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'recovery' => 'Recovery code',
'login' => 'تسجيل الدخول',
'button_remember' => 'تذكرني',
'password_forget' => 'نسيت كلمة المرور؟',
'password_reset' => 'إعادة تعيين كلمة مرورك',
'use_recovery' => 'Or you can use a <a href=":url">recovery code</a>',
'signup_no_account' => 'ليس لديك حساب؟',
'signup' => 'تسجيل الإشتراك',
'create_account' => 'انشئ الحساب الأول عبر <a href=":url">تسجيل الإشتراك</a>',
'change_language_title' => 'تغيير اللغة:',
'change_language' => 'تغيير اللغة إلى :lang',
'password_reset_title' => 'إعادة تعيين كلمة المرور',
'password_reset_email' => 'عنوان البريد',
'password_reset_send_link' => 'أرسل رابط إعادة تعيين كلمة المرور',
'password_reset_password' => 'كلمة المرور',
'password_reset_password_confirm' => 'تأكيد كلمة المرور',
'password_reset_action' => 'إعادة تعيين كلمة المرور',
'password_reset_email_content' => 'اضغط هنا لإعادة تعيين كلمة مرورك:',
'register_title_welcome' => 'مرحبا بك في تطبيق Monica المثبت حديثاً',
'register_create_account' => 'يجب أن تنشئ حساباً لتستخدم Monica',
'register_title_create' => 'انشئ حسابك لـMonica',
'register_login' => 'قم بـ <a href=":url">تسجيل الدخول</a> إذا كان لديك حساب مسبقاً.',
'register_email' => 'أدخل عنوان بريد صالح',
'register_email_example' => 'example@example.com',
'register_firstname' => 'الاسم الأول',
'register_firstname_example' => 'مثال: أحمد',
'register_lastname' => 'الاسم الأخير',
'register_lastname_example' => 'مثال: مراد',
'register_password' => 'كلمة المرور',
'register_password_example' => 'أدخل كلمة مرور آمنة',
'register_password_confirmation' => 'تأكيد كلمة المرور',
'register_action' => 'تسجيل الإشتراك',
'register_policy' => 'بتسجيل إشتراكك تُفيد بأنك قرأت و قبِلت <a href=":url" hreflang=":hreflang">سياسة الخصوصية</a> و <a href=":urlterm" hreflang=":hreflang">شروط الإستخدام</a> الخاصة بنا.',
'register_invitation_email' => 'لأسباب أمنية، الرجاء تحديد البريد الإلكتروني للشخص الذي قمت بدعوته للإنضمام لهذا الحساب. المعلومات موجودة في رسالة الدعوة.',
'confirmation_title' => 'Verify Your Email Address',
'confirmation_fresh' => 'A fresh verification link has been sent to your email address.',
'confirmation_check' => 'Before proceeding, please check your email for a verification link.',
'confirmation_request_another' => 'If you did not receive the email <a :action>click here to request another</a>.',
'confirmation_again' => 'إذا أردت تغيير بريدك الإلكتروني يمكنك <a href=":url" class="alert-link">الضغط هنا</a>.',
'email_change_current_email' => 'البريد الإلكتروني الحالي:',
'email_change_title' => 'قم بتغيير عنوان بريدك',
'email_change_new' => 'بريد إلكتروني جديد',
'email_changed' => 'لقد تم تغيير بريدك الإلكتروني. تحقق من بريدك لتأكيده.',
];

View File

@@ -0,0 +1,12 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'title' => 'تغيرات المنتج',
'note' => 'ملاحظة: لسوء الحظ، هذه الصفحة تظهر فقط باللغة الإنكليزية.',
];

View File

@@ -0,0 +1,42 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'dashboard_blank_title' => 'مرحباً في حسابك!',
'dashboard_blank_description' => 'Monica هو المكان المناسب لتنظيم جميع التفاعلات لديك مع الأشخاص الذين تهتم بأمرهم.',
'dashboard_blank_cta' => 'أضف أول جهة اتصال لك',
'dashboard_blank_illustration' => 'الرسم التوضيحي بواسطة <a href=":url">Freepik</a>',
'notes_title' => 'لم تقم بتأشير أي ملاحظات بعد.',
'tab_recent_calls' => 'المكالمات الأخيرة',
'tab_favorite_notes' => 'الملاحظات المفضلة',
'tab_calls_blank' => 'لم تقم بتسجيل أي مكالمة بعد.',
'tab_debts' => 'الديون',
'tab_debts_blank' => 'لم تقم بتسجيل أي دَين بعد.',
'tab_tasks' => 'المهام',
'tab_tasks_blank' => 'ليس لديك أي مهمة حتى الآن.',
'tasks_add_task_placeholder' => 'What is this task about?',
'tasks_tab_your_contacts' => 'Tasks related to your contacts',
'tasks_tab_your_tasks' => 'Your tasks',
'tasks_add_note' => 'Press <kbd>Enter</kbd> to add the task.',
'task_add_cta' => 'Add a task',
'debts_you_owe' => 'أنت مدين',
'statistics_contacts' => 'جهات الإتصال',
'statistics_activities' => 'الأنشطة',
'statistics_gifts' => 'الهدايا',
'reminders_next_months' => 'الأحداث التي ستقام في الأشهر الثلاث المقبلة',
'reminders_none' => 'ا يوجد تذكير لهذا الشهر.',
'product_changes' => 'Product changes',
'product_view_details' => 'View details',
];

View File

@@ -0,0 +1,36 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Formats for dates, hours, etc.
|--------------------------------------------------------------------------
|
| The following language lines are used for date formatting.
| Be careful to translate it to the right format.
|
| We use Carbon to format dates, see http://carbon.nesbot.com/docs/#api-commonformats
| Format parameters are describe here http://www.php.net/manual/en/function.date.php
|
*/
'short_date_year_time' => 'M d, Y H:i',
'short_date_year' => 'M d, Y',
'short_date' => 'M d',
'short_month' => 'M',
'short_month_year' => 'M Y',
'short_day' => 'D',
'full_date_year' => 'F d, Y',
'full_month' => 'F',
'full_month_year' => 'F Y',
'full_hour' => 'h.i A',
'short_text' => '{text}…',
];

View File

@@ -0,0 +1,38 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'journal_rate' => 'كيف كان يومك؟ يمكنك تقييمه مرة في اليوم.',
'journal_come_back' => 'شكراً. تعال غداً لتقييم يومك مجدداً.',
'journal_description' => 'ملاحظة: الملاحظة تُسجل كِلا التدوينات اليدوية، و التدوينات الفورية مثل الأنشطة التي فعلتَها مع أصدقائك. بينما يمكنك حذف تدوينات المذكرة يدوياً، لا يمكنك حذف الأنشطة إلا بحذفها مباشرة من صفحة أصدقائك.',
'journal_add' => 'أضف تدويناً للمذكرة',
'journal_edit' => 'Edit a journal entry',
'journal_empty' => 'Empty journal',
'journal_created_at' => 'Created at {date}',
'journal_created_automatically' => 'تم إنشائه تلقائياً',
'journal_entry_type_journal' => 'تدوين المذكرة',
'journal_entry_type_activity' => 'نشاط',
'journal_entry_rate' => 'لقد قمتَ بتقييم يومك.',
'journal_add_comment' => 'Care to add a comment (optional)?',
'journal_show_comment' => 'Show comment',
'entry_delete_success' => 'لقد تم حذف تدوين هذه المذكرة بنجاح.',
'journal_add_title' => 'العنوان (اختياري)',
'journal_add_date' => 'التاريخ',
'journal_add_post' => 'تدوين',
'journal_add_cta' => 'حفظ',
'journal_blank_cta' => 'قم بتدوين مذكرتك الأولى',
'journal_blank_description' => 'المذكرة تدَعُك تدون الأحداث التي حصلت لك، لتتذكرها.',
'delete_confirmation' => 'هل أنت متأكد من حذف تدوين هذه المذكرة؟',
'apply_filter' => 'طبق الفلترة',
"start_date" => "تاريخ البدء" ,
"end_date" => "تاريخ الانتهاء" ,
"per_page" => "لكل صفحة" ,
'Sort_order' => 'فرز حسب تاريخ الإنشاء',
"ascending" => "تصاعدي" ,
"descending" => "تنازلي" ,
];

View File

@@ -0,0 +1,29 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
// contact creation
'contact_log_contact_created' => 'Created the contact.',
'settings_log_contact_created_with_name' => 'Added :name as a contact.',
// contat description update
'contact_log_contact_description_updated' => 'Updated the description.',
'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.',
// contact description clear
'contact_log_contact_description_cleared' => 'Cleared the description.',
'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.',
// contact work information update
'contact_log_contact_work_updated' => 'Updated work information.',
'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.',
// company created
'settings_log_company_created' => 'Created a company called :name.',
];

View File

@@ -0,0 +1,53 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'subject_line' => 'تذكير إلى :contact',
'greetings' => 'مرحباً :username',
'want_reminded_of' => 'لقد أردتَ أن يتم تذكيرك بـ:reason',
'for' => 'لـ: :name',
'comment' => 'تعليق: :comment',
'footer_contact_info' => 'أضف، عرض، أكمل، و قم بتغيير معلومات عن جهة الإتصال هذه:',
'footer_contact_info2' => 'عرض ملف :name الشخصي',
'footer_contact_info2_link' => 'See :name’s profile: :url',
'notification_subject_line' => 'لديك حدث قادم',
'notification_description' => 'في :count أيام ( في :date)، سوف يحصل الحدث التالي:',
'stay_in_touch_subject_line' => 'ابقى على إتصال مع :name',
'stay_in_touch_subject_description' => 'لقد طلبتَ أن يتم تذكيرك بالبقاء على اتصال مع :name كل :frequency يوم. | لقد طلبتَ أن يتم تذكيرك بالبقاء على اتصال مع :name كل :frequency أيام.',
'notifications_whoops' => 'المعذرة!',
'notifications_hello' => 'مرحباً!',
'notifications_regards' => 'مع خالص التحيات',
'notifications_footer' => 'إذا كنتَ تواجه مشكلة في الضغط على زر ":actionText"، انسخ والصق العنوان في متصفح الويب الخاص بك: [:actionURL](:actionURL)',
'notifications_rights' => 'جميع الحقوق محفوظة',
'confirmation_email_title' => 'Monica - تأكيد البريد الإلكتروني',
'confirmation_email_intro'=> 'لتأكيد بريدك اضغط على الزر أدناه',
'confirmation_email_button' => 'قم بتأكيد عنوان بريدك',
'confirmation_email_bottom' => 'If you did not create an account, no further action is required.',
'password_reset_title' => 'Monica – Reset Password Notification',
'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.',
'password_reset_button' => 'Reset Password',
'password_reset_expiration' => 'This password reset link will expire in :count minutes.',
'password_reset_bottom' => 'If you did not request a password reset, no further action is required.',
'invitation_title' => 'Monica – You are invited by :name',
'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.',
'invitation_link' => 'To accept the invitation, click on the link below:',
'invitation_button' => 'Accept invitation',
'invitation_expiration' => 'This link will expire in :count days.',
'export_title' => 'Your export is ready',
'export_description' => 'You requested a data export on :date. It is now ready to download.',
'export_download' => 'Download export',
];

View File

@@ -0,0 +1,25 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '❮ السابق',
'next' => 'التالي ❯',
];

View File

@@ -0,0 +1,30 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => 'تمت إعادة تعيين كلمة المرور',
'sent' => 'تم إرسال تفاصيل استعادة كلمة المرور الخاصة بك إلى بريدك الإلكتروني',
'token' => '.رمز استعادة كلمة المرور الذي أدخلته غير صحيح',
'user' => 'لم يتم العثور على أيّ حسابٍ بهذا العنوان الإلكتروني',
'changed' => 'Password changed successfully.',
'invalid' => 'كلمة السر الحالية التي أدخلتها غير صحيحة.',
'throttled' => 'الرجاء الانتظار قبل إعادة المحاولة.',
];

View File

@@ -0,0 +1,539 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
//index
'people_not_found' => 'Contact not found',
'people_list_number_kids' => ':count child|:count children',
'people_list_last_updated' => 'آخر استشارة:',
'people_list_number_reminders' => ':count reminder|:count reminders',
'people_list_blank_title' => 'ليس لديك أي شخص في الحساب الخاص بك بعد',
'people_list_blank_cta' => 'أضف شخص',
'people_list_sort' => 'ترتيب',
'people_list_stats' => ':count contact|:count contacts',
'people_list_firstnameAZ' => 'فرز حسب الاسم الأول A → Z',
'people_list_firstnameZA' => 'فرز حسب الاسم الأول Z → A',
'people_list_lastnameAZ' => 'فرز حسب الاسم الأخير A → Z',
'people_list_lastnameZA' => 'فرز حسب اسم العائلة Z → A',
'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest',
'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest',
'people_list_filter_tag' => 'عرض كل جهات الاتصال ذات الوسم',
'people_list_clear_filter' => 'مسح التصفية',
'people_list_contacts_per_tags' => ':count contact|:count contacts',
'people_list_show_dead' => 'Show deceased people (:count)',
'people_list_hide_dead' => 'Hide deceased people (:count)',
'people_search' => 'Search your contacts…',
'people_search_no_results' => 'No results found',
'people_search_next' => 'Next',
'people_search_prev' => 'Previous',
'people_search_rows_per_page' => 'Rows per page',
'people_search_of' => 'of',
'people_search_page' => 'Page',
'people_search_all' => 'All',
'people_add_new' => 'Add new person',
'people_list_account_usage' => 'استخدام حسابك: :current/:limit جهة اتصال',
'people_list_account_upgrade_title' => 'قم بترقية حسابك لتفعيل كامل إمكانياته.',
'people_list_account_upgrade_cta' => 'قم بالترقية الان',
'people_list_untagged' => 'عرض جهات الاتصال الغير موسومة',
'people_list_filter_untag' => 'عرض كل جهات الاتصال الغير موسومة',
'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.',
// people add
'people_add_title' => 'إضافة شخص جديد',
'people_add_missing' => 'No person found – add a new one now',
'people_add_firstname' => 'الاسم الأول',
'people_add_middlename' => 'Middle name (optional)',
'people_add_lastname' => 'Last name (optional)',
'people_add_email' => 'Email (optional)',
'people_add_nickname' => 'Nickname (optional)',
'people_add_cta' => 'إضافة',
'people_save_and_add_another_cta' => 'تقديم وأضف شخص آخر',
'people_add_success' => ':name تم إضافته بنجاح',
'people_add_gender' => 'الجنس',
'people_delete_success' => 'تم حذف جهة الاتصال',
'people_delete_message' => 'Delete contact',
'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.',
'people_add_birthday_reminder' => 'تمنى :name عيد ميلاد سعيد',
'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday',
'people_add_import' => 'هل تريد <a href=":url">استيراد جهات الاتصال الخاصة بك</a>؟',
'people_edit_email_error' => 'يوجد جهة اتصال في حسابك بنفس عنوان البريد الالكتروني. الرجاء اختيار عنوان آخر.',
'people_export' => 'تصدير ك vCard',
'people_add_reminder_for_birthday' => 'Create an annual birthday reminder',
// show
'section_contact_information' => 'معلومات جهة الاتصال',
'section_personal_activities' => 'الأنشطة',
'section_personal_reminders' => 'رسائل تذكير',
'section_personal_tasks' => 'المهام',
'section_personal_gifts' => 'الهدايا',
'section_personal_notes' => 'الملاحظات',
// archived contacts
'list_link_to_active_contacts' => 'You are viewing archived contacts. See the <a href=":url">list of active contacts</a> instead.',
'list_link_to_archived_contacts' => 'List of archived contacts',
// Header
'me' => 'This is you',
'edit_contact_information' => 'تعديل معلومات الإتصال',
'contact_archive' => 'Archive contact',
'contact_unarchive' => 'Unarchive contact',
'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.',
'call_button' => 'تسجيل مكالمة',
'set_favorite' => 'جهات الاتصال المفضلة يتم وضعها في قمة قائمة جهات الاتصال',
// Stay in touch
'stay_in_touch' => 'ابقى على اتصال',
'stay_in_touch_frequency' => 'ابقى على اتصال يوميا|ابقى على اتصال كل {count} يومًا',
'stay_in_touch_next_date' => 'Next due: {date}',
'stay_in_touch_invalid' => 'يجب أن يكون التواتر عدد أكبر من 0.',
'stay_in_touch_premium' => 'تحتاج إلى ترقية الحساب الخاص بك لاستخدام هذه الميزة',
'stay_in_touch_modal_title' => 'ابقى على اتصال',
'stay_in_touch_modal_desc' => 'يمكن أن نذكرك عن طريق البريد الإلكتروني للبقاء على اتصال مع {firstname} على فترات منتظمة.',
'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days',
// Calls
'modal_call_title' => 'تسجيل مكالمة',
'modal_call_comment' => 'ماذا تحدثتم عنه؟ (اختياري)',
'modal_call_exact_date' => 'تمت المكالمة الهاتفية في',
'modal_call_who_called' => 'Who called?',
'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)',
'calls_add_success' => 'تم حفظ المكالمة الهاتفية.',
'call_delete_confirmation' => 'هل أنت متأكد من حذف هذه المكالمة؟',
'call_delete_success' => 'تم حذف المكالمة بنجاح',
'call_title' => 'المكالمات الهاتفية',
'call_empty_comment' => 'لا تفاصيل',
'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}',
'call_blank_desc' => 'You called {name}',
'call_you_called' => 'You called',
'call_he_called' => '{name} called',
'call_emotions' => 'Emotions:',
// Conversation
'conversation_blank' => 'Record conversations you have with :name on social media, SMS…',
'conversation_delete_link' => 'حذف المحادثة',
'conversation_edit_title' => 'تحرير المحادثة',
'conversation_edit_delete' => 'هل أنت متأكد من حذف هذه المحادثة؟ الحذف دائم.',
'conversation_add_success' => 'The conversation has been successfully added.',
'conversation_edit_success' => 'The conversation has been successfully updated.',
'conversation_delete_success' => 'لقد تم حذف المحادثة بنجاح.',
'conversation_add_title' => 'سَجل محادثة جديدة',
'conversation_add_when' => 'متى تمت هذه المحادثة؟',
'conversation_add_who_wrote' => 'Who sent this message?',
'conversation_add_how' => 'كيف تواصلتما؟',
'conversation_add_you' => 'أنت',
'conversation_add_content' => 'قم بتدوين ما قيل',
'conversation_add_what_was_said' => 'ماذا قلت؟',
'conversation_add_another' => 'إضافة رسالة أخرى',
'conversation_add_error' => 'You must add at least one message.',
'conversation_list_table_messages' => 'الرسائل',
'conversation_list_table_content' => 'محتوى جزئي (آخر رسالة)',
'conversation_list_title' => 'المحادثات',
'conversation_list_cta' => 'سجل المحادثة',
// age - birthday
'birthdate_not_set' => 'Birthday is not set',
'age_approximate_in_years' => ':age سنوات تقريبًا',
'age_exact_in_years' => ':age سنوات',
'age_exact_birthdate' => 'ولد في :date',
// Last called
'last_called' => 'Last called: :date',
'last_talked_to' => 'Last called: {date}',
'last_called_empty' => 'Last called: unknown',
'last_activity_date' => 'Last activity together: :date',
'last_activity_date_empty' => 'Last activity together: unknown',
// additional information
'information_edit_success' => 'تم تحديث الملف الشخصي بنجاح',
'information_edit_title' => 'تعديل معلومات :name الشخصية',
'information_edit_max_size' => 'الحد الأقصى :size كيلوبايت.',
'information_edit_max_size2' => 'Max {size} Kb.',
'information_edit_firstname' => 'الاسم الأول',
'information_edit_lastname' => 'Last name (optional)',
'information_edit_description' => 'Description (optional)',
'information_edit_description_help' => 'تستخدم في قائمة جهات الاتصال لإضافة بعض المعلومات، إذا لزم الأمر.',
'information_edit_unknown' => 'أنا لا أعرف عمر هذا الشخص',
'information_edit_probably' => 'This person is probably…',
'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…',
'information_edit_exact' => 'I know this person’s exact birthday…',
'information_edit_birthdate_label' => 'Birthday',
'information_no_work_defined' => 'لا توجد معلومات العمل',
'information_work_at' => 'لدى :company',
'work_add_cta' => 'تحديث معلومات العمل',
'work_edit_success' => 'Work information updated',
'work_edit_title' => 'تحيث معلومات العمل الخاصة بـ :name',
'work_edit_job' => 'المسمى الوظيفي (اختياري)',
'work_edit_company' => 'الشركة (اختياري)',
'work_information' => 'معلومات العمل',
// food preferences
'food_preferences_add_success' => 'تم حفظ التفضيلات الغذائية',
'food_preferences_edit_description' => 'قد يكون :firstname أو شخص في عائلة :family مصاب بالحساسية، أو لا يريد نوع معين من النبيذ. حدد ذلك هنا لكي تتذكر ذلك في المرة القادمة عندما تدعوهم للعشاء',
'food_preferences_edit_description_no_last_name' => 'قد يكون :firstname مصاب بالحساسية، أو لا يريد نوع معين من النبيذ. حدد ذلك هنا لكي تتذكر ذلك في المرة القادمة عندما تدعوهم للعشاء',
'food_preferences_edit_title' => 'حدد التفضيلات الغذائية',
'food_preferences_edit_cta' => 'حفظ التفضيلات الغذائية',
'food_preferences_title' => 'التفضيلات الغذائية',
'food_preferences_cta' => 'أضف تفضيلات غذائية',
// reminders
'reminders_blank_title' => 'هل هناك شيء ما كنت ترغب أن يتم تذكيرك به بخصوص :name؟',
'reminders_blank_add_activity' => 'إضافة تذكير',
'reminders_add_title' => 'ما الذي ترغب بتذكره عن :name؟',
'reminders_add_description' => 'Please remind me to…',
'reminders_add_next_time' => 'متى تود أن تكون المرة القادمة التي يتم فيها تذكيرك بهذا؟',
'reminders_add_once' => 'ذكرني بهذا مرة واحدة فقط',
'reminders_add_recurrent' => 'ذكرني بهذا كل',
'reminders_add_starting_from' => 'ابتداءً من التاريخ الموضح أعلاه',
'reminders_add_cta' => 'إضافة تذكير',
'reminders_edit_update_cta' => 'تحديث التذكير',
'reminders_add_error_custom_text' => 'يجب إضافة نص لهذا التذكير',
'reminders_create_success' => 'تم بنجاح إضافة تذكير',
'reminders_delete_success' => 'تم حذف التذكير بنجاح',
'reminders_update_success' => 'تم تحديث التذكير بنجاح',
'reminders_add_optional_comment' => 'Optional comment',
'reminder_frequency_day' => 'يوميا|كل :number أيام',
'reminder_frequency_week' => 'أسبوعيا|كل :number أسابيع',
'reminder_frequency_month' => 'شهريا|كل :number أشهر',
'reminder_frequency_year' => 'سنويا|كل :number سنوات',
'reminder_frequency_one_time' => 'في :date',
'reminders_delete_confirmation' => 'هل تريد فعلا حذف هذا التذكير؟',
'reminders_delete_cta' => 'حذف',
'reminders_next_expected_date' => 'على',
'reminders_cta' => 'إضافة تذكير',
'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.',
'reminders_one_time' => 'مرة واحدة',
'reminders_type_week' => 'أسبوع',
'reminders_type_month' => 'شهر',
'reminders_type_year' => 'سنة',
'reminders_birthday' => 'عيد ميلاد :name',
'reminders_free_plan_warning' => 'أنت على الخطة المجانية. لا يتم إرسال رسائل الكترونية في هذه الخطة. قم بترقية حسابك لاستلام رسائل الكترونية.',
// relationships
'relationship_form_add' => 'إضافة علاقة جديدة',
'relationship_form_edit' => 'تعديل علاقة حالية',
'relationship_form_is_with' => 'This person is…',
'relationship_form_is_with_name' => ':name is…',
'relationship_form_add_choice' => 'مع من هذه العلاقة؟',
'relationship_form_create_contact' => 'إضافة شخص جديد',
'relationship_form_associate_contact' => 'جهة اتصال موجودة',
'relationship_form_associate_dropdown' => 'بحث واختيار جهة اتصال موجودة من القائمة المنسدلة أدناه',
'relationship_form_associate_dropdown_placeholder' => 'بحث واختيار جهة اتصال موجودة',
'relationship_form_also_create_contact' => 'إنشاء جهة اتصال لهذا الشخص.',
'relationship_form_add_description' => 'This will let you treat this person like any other contact.',
'relationship_form_add_no_existing_contact' => 'ليس لديك أي جهة اتصال قد تكون على قرابة بـ:name حاليا.',
'relationship_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه العلاقة؟ الحذف دائم.',
'relationship_unlink_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه العلاقة؟ لن يتم حذف هذا الشخص – فقط العلاقة بين الشخصين.',
'relationship_form_add_success' => 'تم تعيين العلاقة بنجاح.',
'relationship_form_deletion_success' => 'تم حذف العلاقة.',
// tasks
'tasks_title' => 'Tasks',
'tasks_blank_title' => 'ليس لديك مهام حتى الآن.',
'tasks_form_title' => 'العنوان',
'tasks_form_description' => 'الوصف (اختياري)',
'tasks_add_task' => 'إضافة مهمة',
'tasks_delete_success' => 'تم حذف المهمة بنجاح',
'tasks_complete_success' => 'تم تغيير حالة المهمة بنجاح',
// activities
'activity_title' => 'الأنشطة',
'activity_type_category_simple_activities' => 'أنشطة بسيطة',
'activity_type_category_sport' => 'رياضة',
'activity_type_category_food' => 'طعام',
'activity_type_category_cultural_activities' => 'أنشطة ثقافية',
'activity_type_just_hung_out' => 'قضينا الوقت معا',
'activity_type_watched_movie_at_home' => 'شاهدنا فيلم في المنزل',
'activity_type_talked_at_home' => 'تحدثنا في المنزل',
'activity_type_did_sport_activities_together' => 'played a sport together',
'activity_type_ate_at_his_place' => 'تناولنا الطعام بمنزلهم',
'activity_type_went_bar' => 'ذهبنا إلى حانة',
'activity_type_ate_at_home' => 'تناولنا الطعام في المنزل',
'activity_type_picnicked' => 'picnicked',
'activity_type_ate_restaurant' => 'تناولنا الطعام في المطعم',
'activity_type_went_theater' => 'ذهبنا إلى دار العرض',
'activity_type_went_concert' => 'ذهبنا إلى حفلة موسيقية',
'activity_type_went_play' => 'ذهبنا إلى مسرحية',
'activity_type_went_museum' => 'ذهبنا إلى المتحف',
'activities_add_activity' => 'إضافة نشاط',
'activities_add_more_details' => 'Add more details',
'activities_add_emotions' => 'Add emotions',
'activities_add_category' => 'Indicate a category',
'activities_add_participants_cta' => 'Add participants',
'activities_item_information' => ':Activity حدث في :date',
'activities_add_title' => 'What did you do with {name}?',
'activities_summary' => 'صف ما فعلته',
'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)',
'activities_add_date_occured' => 'The activity happened on…',
'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)',
'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)',
'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about',
'activities_blank_add_activity' => 'إضافة نشاط',
'activities_add_success' => 'تم إضافة النشاط بنجاح',
'activities_add_error' => 'خطأ عند إضافة نشاط',
'activities_update_success' => 'تم تحديث النشاط بنجاح',
'activities_delete_success' => 'تم حذف النشاط بنجاح',
'activities_who_was_involved' => 'من شارك؟',
'activities_activity' => 'تصنيف النشاط',
'activities_view_activities_report' => 'عرض تقرير الأنشطة',
'activities_profile_title' => 'تقرير عن الأنشطة بينك وبين :name',
'activities_profile_subtitle' => 'لقد سجلت :total_activities نشاطا مع :name إجمالا، و:activities_last_twelve_months خلال الاثناعشر شهرا الماضية حتى الآن.|لقد سجلت :total_activities نشاطا مع :name إجمالا، و:activities_last_twelve_months خلال الاثناعشر شهرا الماضية حتى الآن.',
'activities_profile_year_summary_activity_types' => 'هنا تفاصيل هذا النوع من الأنشطة التي قضيتماها معا في عام :year',
'activities_profile_year_summary' => 'هذا ما فعلتموه معا في عام :year',
'activities_profile_number_occurences' => ':value نشاط|:value أنشطة',
'activities_list_participants' => 'Participants ({total}):',
'activities_list_emotions' => 'Emotions felt:',
'activities_list_date' => 'Happened on',
'activities_list_category' => 'Category:',
// notes
'notes_create_success' => 'تم إنشاء المذكرة بنجاح',
'notes_update_success' => 'تم حفظ المذكرة بنجاح',
'notes_delete_success' => 'تم حذف المذكرة بنجاح',
'notes_add_cta' => 'إضافة ملاحظة',
'notes_favorite' => 'إضافة/إزالة من المفضلة',
'notes_delete_title' => 'حذف ملاحظة',
'notes_delete_confirmation' => 'هل أنت متأكذ من أنك تريد حذف هذه المذكرة؟ الحذف دائم',
// gifts
'gifts_title' => 'الهدايا',
'gifts_add_success' => 'تم بنجاح إضافة هدية',
'gifts_delete_success' => 'تم بنجاح حذف هدية',
'gifts_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه الهدية؟',
'gifts_add_gift' => 'إضافة هدية',
'gifts_link' => 'الرابط',
'gifts_for' => 'For: {name}',
'gifts_delete_cta' => 'حذف',
'gifts_add_title' => 'إدارة الهدايا لـ :name',
'gifts_add_gift_idea' => 'فكرة هدية',
'gifts_add_gift_already_offered' => 'هدية تم تقديمها',
'gifts_add_gift_received' => 'هدية تم استلامها',
'gifts_add_gift_title' => 'ما هذه الهدية؟',
'gifts_add_gift_name' => 'Gift name',
'gifts_add_link' => 'الرابط لصفحة الويب (اختياري)',
'gifts_add_value' => 'القيمة (اختياري)',
'gifts_add_comment' => 'تعليق (اختياري)',
'gifts_add_recipient' => 'Recipient (optional)',
'gifts_add_recipient_field' => 'Recipient',
'gifts_add_photo' => 'Photo (optional)',
'gifts_add_photo_title' => 'Add a photo for this gift',
'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular',
'gifts_delete_title' => 'Delete a gift',
'gifts_ideas' => 'أفكار للهدايا',
'gifts_offered' => 'هدية تم تقديمها',
'gifts_offered_as_an_idea' => 'وضع علامة كفكرة',
'gifts_received' => 'استلام هدية',
'gifts_view_comment' => 'عرض التعليق',
'gifts_mark_offered' => 'ضع علامة بأنه تم عرضها',
'gifts_update_success' => 'تم بنجاح تحديث هدية',
'gifts_add_date' => 'Date (optional)',
// debts
'debt_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الدين؟',
'debt_delete_success' => 'تم بنجاح حذف الدين',
'debt_add_success' => 'تم بنجاح إضافة الدين',
'debt_title' => 'الديون',
'debt_add_cta' => 'إضافة دين',
'debt_you_owe' => 'أنت مدين بـ :amount',
'debt_they_owe' => ':name مدين لك بـ :amount',
'debt_add_title' => 'إدارة الديون',
'debt_add_you_owe' => 'أنت مدين لـ :name',
'debt_add_they_owe' => ':name مدين لك',
'debt_add_amount' => 'مجموع',
'debt_add_reason' => 'للسبب التالي (اختياري)',
'debt_add_add_cta' => 'إضافة دين',
'debt_edit_update_cta' => 'تحديث الدين',
'debt_edit_success' => 'تم بنجاح تحديث الدين',
'debts_blank_title' => 'إدارة الديون التي تدين بها لـ:name أو يدين بها :name لك',
// tags
'tag_edit' => 'تعديل وسم',
'tag_add' => 'Add tags',
'tag_add_search' => 'Add or search tags',
'tag_no_tags' => 'No tags yet',
// Introductions
'introductions_sidebar_title' => 'كيف إلتقيتم',
'introductions_blank_cta' => 'حدد كيف قابلت :name',
'introductions_title_edit' => 'كيف التقيت بـ :name؟',
'introductions_additional_info' => 'اشرح كيف وأين إلتقيتم',
'introductions_edit_met_through' => 'هل عرّفك أحد بهذا الشخص؟',
'introductions_no_met_through' => 'لا أحد',
'introductions_first_met_date' => 'تاريخ اللقاء',
'introductions_no_first_met_date' => 'لا أعرف تاريخ اللقاء الأول',
'introductions_first_met_date_known' => 'هذا هو تاريخ اللقاء الأول',
'introductions_add_reminder' => 'أضف تذكير للاحتفال بذكرى هذه المناسبة',
'introductions_update_success' => 'لقد حدثت معلومات كيفية لقائك هذا الشخص بنجاح',
'introductions_met_through' => 'التقينا بواسطة <a href=":url">:name</a>',
'introductions_met_date' => 'التقينا في :date',
'introductions_reminder_title' => 'الذكرى السنوية لليوم الذي التقيتم فيه للمرة الأولى',
// Deceased
'deceased_reminder_title' => 'الذكرى السنوية لوفاة :name',
'deceased_mark_person_deceased' => 'Mark this as deceased',
'deceased_know_date' => 'I know the date that this person died',
'deceased_add_reminder' => 'إضافة تذكير لهذا التاريخ',
'deceased_label' => 'متوفى',
'deceased_date_label' => 'Deceased date',
'deceased_label_with_date' => 'توفي بتاريخ :date',
'deceased_age' => 'العمر عند الوفاة',
// Contact information
'contact_info_title' => 'معلومات جهة الاتصال',
'contact_info_form_content' => 'المحتوى',
'contact_info_form_contact_type' => 'نوع الاتصال',
'contact_info_form_personalize' => 'إضفاء طابع شخصي',
'contact_info_address' => 'يعيش في',
// Addresses
'contact_address_title' => 'عناوين',
'contact_address_form_name' => 'تسمية (اختياري)',
'contact_address_form_street' => 'الشارع (اختياري)',
'contact_address_form_city' => 'المدينة (اختياري)',
'contact_address_form_province' => 'المنطقة (اختياري)',
'contact_address_form_postal_code' => 'الرمز البريدي (اختياري)',
'contact_address_form_country' => 'البلد (اختياري)',
'contact_address_form_latitude' => 'Latitude (numbers only) (optional)',
'contact_address_form_longitude' => 'Longitude (numbers only) (optional)',
// Pets
'pets_kind' => 'نوع الحيوان الأليف',
'pets_name' => 'الاسم (اختيارى)',
'pets_create_success' => 'The pet has been successfully added',
'pets_update_success' => 'تم تحديث الحيوان الأليف',
'pets_delete_success' => 'تم حذف الحيوان الأليف',
'pets_title' => 'الحيوانات الأليفة',
'pets_reptile' => 'زواحف',
'pets_bird' => 'طيور',
'pets_cat' => 'قطط',
'pets_dog' => 'كلاب',
'pets_fish' => 'أسماك',
'pets_hamster' => 'هامستر',
'pets_horse' => 'حصان',
'pets_rabbit' => 'أرنب',
'pets_rat' => 'جرذ',
'pets_small_animal' => 'حيوانات صغيرة',
'pets_other' => 'غير ذلك',
// life events
'life_event_list_tab_life_events' => 'أحداث الحياة',
'life_event_list_tab_other' => 'Notes, reminders, …',
'life_event_list_title' => 'أحداث الحياة',
'life_event_blank' => 'قم بتسجيل ما يحدث في حياة {name} كمرجع لك في المستقبل.',
'life_event_list_cta' => 'إضافة حدث الحياة',
'life_event_create_category' => 'كل الفئات',
'life_event_create_life_event' => 'إضافة حدث الحياة',
'life_event_create_default_title' => 'العنوان (اختياري)',
'life_event_create_default_story' => 'القصة (اختياري)',
'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.',
'life_event_create_default_description' => 'أضف معلومات حول ما تعرفه',
'life_event_create_add_yearly_reminder' => 'أضف تذكيراً سنوياً لهذا الحدث',
'life_event_create_success' => 'تمت إضافة الحدث',
'life_event_delete_title' => 'حذف حدث الحياة',
'life_event_delete_description' => 'هل أنت متأكد من أنك تريد حذف هذا الحدث؟ الحذف دائم.',
'life_event_delete_success' => 'تمت حذف هذا الحدث',
'life_event_date_it_happened' => 'التاريخ الذي حدث فيه',
'life_event_category_work_education' => 'Work & education',
'life_event_category_family_relationships' => 'Family & relationships',
'life_event_category_home_living' => 'Home & living',
'life_event_category_health_wellness' => 'Health & wellness',
'life_event_category_travel_experiences' => 'Travel & experiences',
'life_event_sentence_new_job' => 'بدأ في عمل جديد',
'life_event_sentence_retirement' => 'تقاعد',
'life_event_sentence_new_school' => 'بدأت المدرسة',
'life_event_sentence_study_abroad' => 'درست خارجاً',
'life_event_sentence_volunteer_work' => 'بدأت في التطوع',
'life_event_sentence_published_book_or_paper' => 'نشرت مقال',
'life_event_sentence_military_service' => 'بدأت الخدمة العسكرية',
'life_event_sentence_new_relationship' => 'بدأت في علاقة',
'life_event_sentence_engagement' => 'خطبت',
'life_event_sentence_marriage' => 'تزوجت',
'life_event_sentence_anniversary' => 'ذكرى',
'life_event_sentence_expecting_a_baby' => 'انتظار مولود',
'life_event_sentence_new_child' => 'رزق بمولود',
'life_event_sentence_new_family_member' => 'إضافة فرد جديد في العائلة',
'life_event_sentence_new_pet' => 'Got a pet',
'life_event_sentence_end_of_relationship' => 'أنهيت علاقة',
'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one',
'life_event_sentence_moved' => 'إنتقل',
'life_event_sentence_bought_a_home' => 'اشتريت منزل',
'life_event_sentence_home_improvement' => 'Made a home improvement',
'life_event_sentence_holidays' => 'ذهبت في عطلة',
'life_event_sentence_new_vehicle' => 'Got a new vehicle',
'life_event_sentence_new_roommate' => 'Got a roommate',
'life_event_sentence_overcame_an_illness' => 'Overcame an illness',
'life_event_sentence_quit_a_habit' => 'أقلعت عن عادة',
'life_event_sentence_new_eating_habits' => 'Started new eating habits',
'life_event_sentence_weight_loss' => 'فقدت الوزن',
'life_event_sentence_wear_glass_or_contact' => 'بدأت في ارتداء النظارات أو العدسات',
'life_event_sentence_broken_bone' => 'كسرت عظماً',
'life_event_sentence_removed_braces' => 'أزلت تقويم الأسنان',
'life_event_sentence_surgery' => 'Had surgery',
'life_event_sentence_dentist' => 'ذهبت لطبيب الأسنان',
'life_event_sentence_new_sport' => 'بدأت في رياضة',
'life_event_sentence_new_hobby' => 'بدأت هواية',
'life_event_sentence_new_instrument' => 'تعلم آلة جديدة',
'life_event_sentence_new_language' => 'تعلمت لغة جديدة',
'life_event_sentence_tattoo_or_piercing' => 'تلقى وشماً أو ثقب',
'life_event_sentence_new_license' => 'حصلتُ على رخصة',
'life_event_sentence_travel' => 'سافرت',
'life_event_sentence_achievement_or_award' => 'تلقى إنجازاً أو جائزة',
'life_event_sentence_changed_beliefs' => 'غيرت معتقداتي',
'life_event_sentence_first_word' => 'تحدث للمرة الأولى',
'life_event_sentence_first_kiss' => 'Kissed for the first time',
// documents
'document_list_title' => 'Documents',
'document_list_cta' => 'Upload document',
'document_list_blank_desc' => 'Here you can store documents related to this person.',
'document_upload_zone_cta' => 'Upload a file',
'document_upload_zone_progress' => 'Uploading the document…',
'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.',
// Photos
'photo_title' => 'Photos',
'photo_list_title' => 'Related photos',
'photo_list_cta' => 'Upload photo',
'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!',
'photo_upload_zone_cta' => 'Upload a photo',
'photo_current_profile_pic' => 'Current profile picture',
'photo_make_profile_pic' => 'Make profile picture',
'photo_delete' => 'Delete photo',
'photo_next' => 'Next photo ❯',
'photo_previous' => '❮ Previous photo',
// Avatars
'avatar_change_title' => 'Change your avatar',
'avatar_question' => 'Which avatar would you like to use?',
'avatar_default_avatar' => 'The default avatar',
'avatar_adorable_avatar' => 'The Adorable avatar',
'avatar_gravatar' => 'The Gravatar associated with the email address of this person. <a href="https://gravatar.com/">Gravatar</a> is a global system that lets users associate email addresses with photos.',
'avatar_current' => 'Keep the current avatar',
'avatar_photo' => 'From a photo that you upload',
'avatar_crop_new_avatar_photo' => 'Crop new avatar photo',
// emotions
'emotion_this_made_me_feel' => 'This made you feel…',
// logs
'auditlogs_link' => 'History',
'auditlogs_title' => 'Everything that happened to :name',
'auditlogs_breadcrumb' => 'History',
'auditlogs_author' => 'By :name on :date',
// contact field label
'contact_field_label_home' => 'Home',
'contact_field_label_work' => 'Work',
'contact_field_label_cell' => 'Mobile',
'contact_field_label_fax' => 'Fax',
'contact_field_label_pager' => 'Pager',
'contact_field_label_main' => 'Main',
'contact_field_label_other' => 'Other',
'contact_field_label_personal' => 'Personal',
];

View File

@@ -0,0 +1,16 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'type_birthday' => 'تمنى يوم مولد سعيد لـ',
'type_phone_call' => 'اتصل',
'type_lunch' => 'تناول الغذاء مع',
'type_hangout' => 'قضاء الوقت مع',
'type_email' => 'أرسل رسالة',
'type_birthday_kid' => 'Wish happy birthday to the child of',
];

View File

@@ -0,0 +1,557 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'sidebar_settings' => 'إعدادات الحساب',
'sidebar_personalization' => 'التخصيص',
'sidebar_settings_storage' => 'Storage',
'sidebar_settings_export' => 'تصدير البيانات',
'sidebar_settings_users' => 'المستخدمين',
'sidebar_settings_subscriptions' => 'الإشتراك',
'sidebar_settings_import' => 'استيراد البيانات',
'sidebar_settings_tags' => 'Tag management',
'sidebar_settings_api' => 'API (واجهة برمجة التطبيق)',
'sidebar_settings_dav' => 'DAV Resources',
'sidebar_settings_security' => 'الأمن',
'sidebar_settings_auditlogs' => 'Audit logs',
'title_general' => 'General Information',
'title_i18n' => 'International settings',
'title_layout' => 'Layout',
'me_title' => 'Me as a contact',
'me_help' => 'This is the contact that represents <em>you</em> in Monica',
'me_select' => 'Select a contact',
'me_no_contact' => 'No contact selected yet.',
'me_select_click' => 'Click here to select a contact.',
'me_remove_contact' => 'Remove the association',
'me_choose' => 'Choose yourself',
'me_choose_placeholder' => 'Choose yourself',
'export_title' => 'قم بتصدير بيانات حسابك',
'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.',
'export_title_sql' => 'Export to SQL',
'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.',
'export_sql_cta' => 'Export to SQL',
'export_sql_link_instructions' => 'Note: <a href=":url">read the instructions</a> to learn more about importing this file to your instance.',
'export_title_json' => 'Export to Json',
'export_submitted' => 'Your export has been submitted, it will be available in a few moment…',
'export_json_explanation' => 'Exporting your data in Json format for backup.',
'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:',
'export_json_cta' => 'Export to Json',
'export_header_type' => 'Type',
'export_header_timestamp' => 'Creation date',
'export_header_status' => 'Status',
'export_header_actions' => 'Actions',
'export_last_title' => 'Last exports',
'export_empty_title' => 'No exports yet',
'export_type_json' => 'Json export',
'export_type_sql' => 'SQL export',
'export_status_todo' => 'Submitted',
'export_status_doing' => 'Doing',
'export_status_done' => 'Done',
'export_status_failed' => 'Failed',
'export_not_done' => 'Download impossible, this export is not done yet.',
'firstname' => 'الاسم الأول',
'lastname' => 'الاسم الأخير',
'name_order' => 'ترتيب الاسم',
'name_order_firstname_lastname' => '<First name> <Last name> – John Doe',
'name_order_lastname_firstname' => '<Last name> <First name> – Doe John',
'name_order_firstname_lastname_nickname' => '<First name> <Last name> (<Nickname>) – John Doe (Rambo)',
'name_order_firstname_nickname_lastname' => '<First name> (<Nickname>) <Last name> – John (Rambo) Doe',
'name_order_lastname_firstname_nickname' => '<Last name> <First name> (<Nickname>) – Doe John (Rambo)',
'name_order_lastname_nickname_firstname' => '<Last name> (<Nickname>) <First name> – Doe (Rambo) John',
'name_order_nickname_firstname_lastname' => '<Nickname> (<First name> <Last name>) – Rambo (John Doe)',
'name_order_nickname_lastname_firstname' => '<Nickname> (<Last name> <First name>) – Rambo (Doe John)',
'name_order_nickname' => '<Nickname> – Rambo',
'currency' => 'العملة',
'name' => 'اسمك: :اسم',
'email' => 'البريد الإلكتروني',
'email_placeholder' => 'أدخل البريد الإلكتروني',
'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.',
'timezone' => 'المنطقة الزمنية',
'temperature_scale' => 'Temperature scale',
'temperature_scale_fahrenheit' => 'Fahrenheit (°F)',
'temperature_scale_celsius' => 'Celsius (°C)',
'layout' => 'التصميم',
'layout_small' => 'الحد الأقصى 1200 بكسل',
'layout_big' => 'العرض الكامل للمستعرض',
'save' => 'تحديث التفضيلات',
'delete_title' => 'احذف حسابك',
'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.',
'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.',
'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.',
'reset_title' => 'أعد تعيين حسابك',
'reset_cta' => 'إعادة تعيين الحساب',
'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.',
'reset_success' => 'Your account has been reset successfully.',
'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.',
'delete_cta' => 'حذف الحساب',
'settings_success' => 'تم تحديث التفضيلات!',
'locale' => 'اللغة المستخدمة في هذا التطبيق',
'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow <a href=":url" target="_blank" lang="en">this link for more information</a>.',
'locale_ar' => 'العربية',
'locale_cs' => 'التشيكية',
'locale_de' => 'الألمانية',
'locale_el' => 'Greek',
'locale_en' => 'الإنجليزية',
'locale_en-GB' => 'English (United Kingdom)',
'locale_es' => 'الإسبانية',
'locale_fr' => 'الفرنسية',
'locale_he' => 'العبرية',
'locale_hr' => 'الكرواتية',
'locale_id' => 'Indonesian',
'locale_it' => 'الإيطالية',
'locale_ja' => 'Japanese',
'locale_nl' => 'الهولندية',
'locale_pt' => 'البرتغالية',
'locale_pt-BR' => 'Portuguese, Brazil',
'locale_ru' => 'الروسية',
'locale_sv' => 'Swedish',
'locale_vi' => 'Vietnamese',
'locale_zh' => 'الصينية المبسطة',
'locale_zh-TW' => 'Chinese Traditional',
'locale_tr' => 'التركية',
'security_title' => 'الأمن',
'security_help' => 'قم بتغيير المسائل الأمنية للحساب الخاص بك.',
'password_change' => 'Change your password',
'password_current' => 'كلمة السر الحالية',
'password_current_placeholder' => 'أدخل كلمة مرورك الحالية',
'password_new1' => 'كلمة مرور جديدة',
'password_new1_placeholder' => 'Enter your new password',
'password_new2' => 'Confirm your new password',
'password_new2_placeholder' => 'Retype your new password',
'password_btn' => 'تغيير كلمة المرور',
'2fa_title' => 'المصادقة الثنائية',
'2fa_otp_title' => 'تطبيق المصادقة الثنائية',
'2fa_enable_title' => 'تمكين المصادقة الثنائية',
'2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.',
'2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:',
'2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:',
'2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:',
'2fa_enable_success' => 'تم تفعيل المصادقة الثنائية',
'2fa_enable_error' => 'حدث خطأ عند محاولة تنشيط المصادقة الثنائية',
'2fa_enable_error_already_set' => 'تم تفعيل المصادقة الثنائية مسبقاً',
'2fa_disable_title' => 'تعطيل المصادقة الثنائية',
'2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!',
'2fa_disable_success' => 'تم تعطيل المصادقة الثنائية',
'2fa_disable_error' => 'حدث خطأ عند محاولة تعطيل المصادقة الثنائية',
'webauthn_title' => 'Security key — WebAuthn protocol',
'webauthn_enable_description' => 'Add a new security key',
'webauthn_key_name_help' => 'Give your key a name.',
'webauthn_key_name' => 'Key name:',
'webauthn_success' => 'Your key is detected and validated.',
'webauthn_last_use' => 'Last use: {timestamp}',
'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?',
'webauthn_delete_success' => 'Key deleted',
'webauthn_insertKey' => 'Insert your security key.',
'webauthn_buttonAdvise' => 'If your security key has a button, press it.',
'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.',
'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.',
'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.',
'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.',
'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.',
'recovery_title' => 'Recovery codes',
'recovery_show' => 'Get recovery codes',
'recovery_copy_help' => 'Copy codes in your clipboard',
'recovery_help_intro' => 'These are your recovery codes:',
'recovery_help_information' => 'You can use each recovery code once.',
'recovery_clipboard' => 'Codes copied to the clipboard.',
'recovery_generate' => 'Generate new codes…',
'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.',
'recovery_already_used_help' => 'This code has already been used.',
'users_list_title' => 'المستخدمين الذين لديهم حق الوصول إلى حسابك',
'users_list_add_user' => 'دعوة مستخدم جديد',
'users_list_you' => 'هذا أنت',
'users_list_invitations_title' => 'دعوات معلقة',
'users_list_invitations_explanation' => 'فيما يلي الأشخاص الذين قمتَ بدعوتهم للإنضمام إلى Monica كمتعاون.',
'users_list_invitations_invited_by' => 'دعوة من :name',
'users_list_invitations_sent_date' => 'تم الإرسال في :date',
'users_blank_title' => 'أنت الشخص الوحيد الذي لديه حق الوصول إلى هذا الحساب.',
'users_blank_add_title' => 'هل ترغب في دعوة شخص آخر؟',
'users_blank_description' => 'هذا الشخص سيكون له نفس حق الوصول الذي لديك، وسيكون قادراً على إضافة أو تحرير أو حذف معلومات جهة الاتصال.',
'users_blank_cta' => 'دعوة شخص ما',
'users_add_title' => 'Invite a new user to your account by email',
'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.',
'users_add_email_field' => 'أدخل عنوان البريد الإلكتروني للشخص الذي تريد دعوته',
'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.',
'users_add_cta' => 'دعوة المستخدم عن طريق البريد الإلكتروني',
'users_accept_title' => 'قبول الدعوة و إنشاء حساب جديد',
'users_error_please_confirm' => 'الرجاء التأكيد بأنك تريد دعوة هذا المستخدم قبل مواصلة الدعوة',
'users_error_email_already_taken' => 'هذا البريد الإلكتروني موجود بالفعل. الرجاء إدخال بريد إلكتروني آخر',
'users_error_already_invited' => 'لقد قمت بدعوة هذا المستخدم مسبقاً. الرجاء اختيار بريد آخر.',
'users_error_email_not_similar' => 'هذا ليس عنوان بريد الشخص الذي قمتَ بدعوته.',
'users_invitation_deleted_confirmation_message' => 'لقد تم حذف الدعوة بنجاح',
'users_invitations_delete_confirmation' => 'هل أنت متأكد من حذف هذه الدعوة؟',
'users_list_delete_confirmation' => 'هل أنت متأكد من حذف هذا المستخدم من حسابك؟',
'users_invitation_need_subscription' => 'إضافة المزيد من المستخدمين يتطلب الإشتراك.',
'subscriptions_account_current_plan' => 'خطتك الحالية',
'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:',
'subscriptions_account_current_paid_plan' => 'أنت على الخطة :name. شكراً جزيلاً لكونك مشتركاً.',
'subscriptions_account_next_billing_title' => 'Next bill',
'subscriptions_account_next_billing' => 'Your subscription will auto-renew on <strong>:date</strong>.',
'subscriptions_account_bill_monthly' => 'We’ll bill you <strong>:price</strong> for another <strong>month</strong>.',
'subscriptions_account_bill_annual' => 'We’ll bill you <strong>:price</strong> for another <strong>year</strong>.',
'subscriptions_account_change' => 'Change plan',
'subscriptions_account_cancel_title' => 'Cancel subscription',
'subscriptions_account_cancel_action' => 'Cancel subscription',
'subscriptions_account_cancel' => 'You can <a href=":url">cancel subscription</a> anytime.',
'subscriptions_account_free_plan' => 'أنت في الخطة المجانية.',
'subscriptions_account_free_plan_upgrade' => 'تستطيع ترقية حسابك لخطة :name، و التي تكلف :price$ شهرياً. هذه هي المنافع:',
'subscriptions_account_free_plan_benefits_users' => 'عدد لا نهائي من المستخدمين',
'subscriptions_account_free_plan_benefits_reminders' => 'تذكير بواسطة البريد اإلكتروني',
'subscriptions_account_free_plan_benefits_import_data_vcard' => 'قم بإستيراد جهات اتصالك مع vCard',
'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.',
'subscriptions_account_upgrade' => 'قم بترقية حسابك',
'subscriptions_account_upgrade_title' => 'قم بترقية Monica اليوم و احصل على علاقات أكثر أهمية.',
'subscriptions_account_upgrade_choice' => 'اختر خطة أدناه و انضم إلى :customers الأشخاص الذين قاموا بترقية Monica الخاص بهم.',
'subscriptions_account_update_title' => 'Update Monica subscription',
'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.',
'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.',
'subscriptions_account_invoices' => 'الفواتير',
'subscriptions_account_invoices_download' => 'تنزيل',
'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate',
'subscriptions_account_payment' => 'أي خيار للدفع يناسبك أكثر؟',
'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please <a href=":url">confirm your payment</a>.',
'subscriptions_downgrade_title' => 'قم بخفض مرتبة حسابك للخطة المجانية',
'subscriptions_downgrade_limitations' => 'خطتك المجانية فيها قيود. لتمكين خفض المرتبة، يجب أن تجتاز القائمة أدناه:',
'subscriptions_downgrade_rule_users' => 'يجب أن يكون لديك مستخدم 1 فقط في حسابك',
'subscriptions_downgrade_rule_users_constraint' => 'حالياً لديك <a href=":url">مستخدم واحد</a> في حسابك. | لديك حالياً <a href=":url">:count مستخدمين</a> في حسابك.',
'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations',
'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have <a href=":url">1 pending invitation</a>.|You currently have <a href=":url">:count pending invitations</a>.',
'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts',
'subscriptions_downgrade_rule_contacts_constraint' => 'لديك حالياً <a href=":url"> جهة اتصال واحدة</a>.| لديك حالياً <a href=":url">:count جهات اتصال</a>.',
'subscriptions_downgrade_rule_contacts_archive' => 'We can also <a href=":url">archive all your contacts for you</a> – that would clear this rule and let you proceed with your account’s downgrade process.',
'subscriptions_downgrade_cta' => 'خفض المرتبة',
'subscriptions_downgrade_success' => 'لقد عدتَ للخطة المجانية!',
'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.',
'subscriptions_back' => 'العودة للإعدادات',
'subscriptions_upgrade_title' => 'قم بترقية حسابك',
'subscriptions_upgrade_choose' => 'لقد اخترت خطة :plan.',
'subscriptions_upgrade_infos' => 'لا يمكننا أن نكون أكثر سعادة. أدخل بيانات دفعك أدناه.',
'subscriptions_upgrade_name' => 'الإسم على البطاقة',
'subscriptions_upgrade_zip' => 'الرمز البريدي',
'subscriptions_upgrade_credit' => 'بطاقة الإئتمان أو بطاقة الصراف',
'subscriptions_upgrade_submit' => 'Pay {amount}',
'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.',
'subscriptions_upgrade_charge_handled' => 'الدفع يتولاه <a href=":url">Stripe</a>. لا تلمس معلومات البطاقات خادمنا.',
'subscriptions_upgrade_success' => 'شكراً لك! أنت مشترك الآن.',
'subscriptions_upgrade_thanks' => 'مرحباً بك في مجتمع الأشخاص الذين يحاولون جعل العالم مكاناً أفضل.',
'subscriptions_payment_confirm_title' => 'Confirm your :amount payment',
'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.',
'subscriptions_payment_succeeded_title' => 'Payment Successful',
'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.',
'subscriptions_payment_cancelled_title' => 'Payment Cancelled',
'subscriptions_payment_cancelled' => 'This payment was cancelled.',
'subscriptions_payment_error_name' => 'Please provide your name.',
'subscriptions_payment_success' => 'The payment was successful.',
'subscriptions_pdf_title' => 'اشتراكك :name الشهري',
'subscriptions_plan_frequency_year' => ':amount / year',
'subscriptions_plan_frequency_month' => ':amount / month',
'subscriptions_plan_choose' => 'اختر هذه الخطة',
'subscriptions_plan_year_title' => 'دفع سنوي',
'subscriptions_plan_year_bonus' => 'راحة البال لسنة كاملة',
'subscriptions_plan_month_title' => 'دفع شهري',
'subscriptions_plan_month_bonus' => 'قم بالإلغاء في أي وقت',
'subscriptions_plan_include1' => 'تشمل الترقية الخاصة بك:',
'subscriptions_plan_include2' => 'عدد لا نهائي من جهات الإتصال • عدد لا نهائي من المستخدمين • تذكير عبر البريد الإلكتروني • استيراد بـvCard • تخصيص صفحة جهة الإتصال',
'subscriptions_plan_include3' => '100% من الأرباح تذهب لتطوير هذا المشروع المفتوح المصدر الرائع.',
'subscriptions_help_title' => 'تفاصيل إضافية قد تكون مهتماً بمعرفتها',
'subscriptions_help_opensource_title' => 'ما هو المشروع المفتوح المصدر؟',
'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.',
'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?',
'subscriptions_help_limits_plan' => 'نعم. الخطط المجانية تدَعُك تُدِير:number جهات إتصال.',
'subscriptions_help_discounts_title' => 'هل لديكم تخفيضات لأسباب غير ربحية أو للتعليم؟',
'subscriptions_help_discounts_desc' => 'نعم لدينا! تطبيق Monica مجاني للطلاب، و مجاني لأسباب غير ربحية و للمؤسسات الخيرية. فقط تواصل مع <a href=":support">الدعم</a> بدليل على حالتك و سنقوم بتطبيق هذه الحالة الخاصة في حسابك.',
'subscriptions_help_change_title' => 'ماذا إذا قمتُ بتغيير رأيي؟',
'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.',
'stripe_error_card' => 'تم رفض بطاقتك. الرسالة المنحدرة: :message',
'stripe_error_api_connection' => 'فشل الإتصال مع Stripe. حاول مجدداً لاحقاً.',
'stripe_error_rate_limit' => 'يوجد عدد كبير من الطلبات على Stripe الآن. حاول مجدداً لاحقاً.',
'stripe_error_invalid_request' => 'Invalid parameters. Try again later.',
'stripe_error_authentication' => 'خطأ مصادقة مع Stripe',
'import_title' => 'استيراد جهات الاتصال إلى حسابك',
'import_cta' => 'تحميل جهات الإتصال',
'import_stat' => 'قمت بإستيراد :number ملفات حتى الآن.',
'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)',
'import_view_report' => 'عرض التقرير',
'import_in_progress' => 'الاستيراد قيد التقدم. أعد تحميل الصفحة في دقيقة واحدة.',
'import_upload_title' => 'قم بإستيراد جهات اتصالك من ملف vCard',
'import_upload_rules_desc' => 'و لكن لدينا بعض الشروط:',
'import_upload_rule_format' => 'نحن ندعم ملفات <code>.vcard</code> و <code>.vcf</code>.',
'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.',
'import_upload_rule_instructions' => 'Export instructions for <a href=":url1" target="_blank" rel="noopener noreferrer">macOS Contacts.app</a> and <a href=":url2" target="_blank" rel="noopener noreferrer">Google Contacts</a>.',
'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.',
'import_upload_rule_limit' => 'Files are limited to 10 MB.',
'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.',
'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.',
'import_upload_form_file' => 'ملف <code>.vcf</code> أو <code>.vCard</code> الخاص بك:',
'import_upload_behaviour' => 'سلوك الاستيراد:',
'import_upload_behaviour_add' => 'Add new contacts and skip existing',
'import_upload_behaviour_replace' => 'استبدال جهات الاتصال الموجودة',
'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.',
'import_report_title' => 'تقرير الإستيراد',
'import_report_date' => 'تاريخ الإستيراد',
'import_report_type' => 'نوع الإستيراد',
'import_report_number_contacts' => 'عدد جهات الإتصال في الملف',
'import_report_number_contacts_imported' => 'عدد جهات الإتصال المستوردة',
'import_report_number_contacts_skipped' => 'عدد جهات الإتصال المتخطى عنها',
'import_report_status_imported' => 'تم استيراده',
'import_report_status_skipped' => 'تم تخطيه',
'import_vcard_parse_error' => 'Error when parsing the vCard entry',
'import_vcard_contact_exist' => 'جهة الإتصال موجودة مسبقاً',
'import_vcard_contact_no_firstname' => 'No first name (mandatory)',
'import_vcard_file_not_found' => 'لم يتم إيجاد الملف',
'import_vcard_unknown_entry' => 'جهة اتصال غير معروفة',
'import_vcard_file_no_entries' => 'لا يوجد إدخالات في الملف',
'import_blank_title' => 'لم تقم بإستيراد أي جهات إتصال بعد.',
'import_blank_question' => 'هل تريد استيراد جهات الإتصال الآن؟',
'import_blank_description' => 'يمكننا استيراد ملفات vCard التي يمكن أن تحصل عليها من جهات اتصال Google أو مدير جهات الاتصال الخاصة بك.',
'import_blank_cta' => 'استيراد vCard',
'import_need_subscription' => 'استيراد البيانات يتطلب اشتراكاً.',
'tags_list_title' => 'Tags',
'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.',
'tags_list_contact_number' => '1 جهة اتصال|:count جهات اتصال',
'tags_list_delete_success' => 'The tag has been successfully deleted',
'tags_list_edit_success' => 'The tag has been successfully updated',
'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.',
'tags_blank_title' => 'Tags are a great way of categorizing your contacts.',
'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.',
'api_title' => 'الوصول إلى API',
'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.',
'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See <a href=":url">API documentation</a>.',
'api_endpoint' => 'The API endpoint for this Monica instance is:',
'api_personal_access_tokens' => 'Personal access tokens',
'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.',
'api_token_title' => 'Personal Access Tokens',
'api_token_create_new' => 'Create New Token',
'api_token_not_created' => 'You have not created any personal access tokens.',
'api_token_name' => 'Token name',
'api_token_expire' => 'Expires at {date}',
'api_token_delete' => 'حذف',
'api_token_create' => 'Create Token',
'api_token_scopes' => 'Scopes',
'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.',
'api_oauth_clients' => 'Your OAuth clients',
'api_oauth_clients_desc' => 'هذا القسم يتيح لك تسجيل عملاء OAuth الخاصين بك.',
'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See <a href="{url}">Laravel Passport documentation</a> for more information.',
'api_oauth_title' => 'OAuth Clients',
'api_oauth_create_new' => 'إنشاء عميل جديد',
'api_oauth_edit' => 'Edit Client',
'api_oauth_not_created' => 'لم تقم بإنشاء أي عملاء OAuth.',
'api_oauth_clientid' => 'معرف العميل',
'api_oauth_name' => 'الاسم',
'api_oauth_name_help' => 'شيء سيَتَعرف عليه مستخدمونك و يثقون به.',
'api_oauth_secret' => 'سري',
'api_oauth_create' => 'إنشاء عميل',
'api_oauth_redirecturl' => 'رابط إعادة التوجيه',
'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.',
'api_authorized_clients' => 'قائمة العملاء المصرح بهم',
'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.',
'api_authorized_clients_title' => 'التطبيقات المصرحة بها',
'api_authorized_clients_none' => 'There are no authorized clients yet.',
'api_authorized_clients_name' => 'الاسم',
'api_authorized_clients_scopes' => 'Scopes',
'personalization_tab_title' => 'قم بتخصيص حسابك',
'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.',
'personalization_contact_field_type_title' => 'أنواع حقول جهة الإتصال',
'personalization_contact_field_type_add' => 'أضف نوع حقل جديد',
'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.',
'personalization_contact_field_type_table_name' => 'الاسم',
'personalization_contact_field_type_table_protocol' => 'النظام',
'personalization_contact_field_type_table_actions' => 'إجراءات',
'personalization_contact_field_type_modal_title' => 'إضافة نوع حقل جهة اتصال جديد',
'personalization_contact_field_type_modal_edit_title' => 'تحرير نوع حقل جهة اتصال موجود',
'personalization_contact_field_type_modal_delete_title' => 'حذف نوع حقل جهة اتصال موجود',
'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.',
'personalization_contact_field_type_modal_name' => 'الاسم',
'personalization_contact_field_type_modal_protocol' => 'النظام (إختياري)',
'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.',
'personalization_contact_field_type_modal_icon' => 'الأيقونة (اختياري)',
'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.',
'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.',
'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.',
'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.',
'personalization_genders_title' => 'أنواع الجنس',
'personalization_genders_add' => 'إضافة نوع جنس جديد',
'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.',
'personalization_genders_modal_add' => 'إضافة نوع جنس',
'personalization_genders_modal_edit' => 'تحديث نوع الجنس',
'personalization_genders_modal_name' => 'Name',
'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.',
'personalization_genders_modal_sex' => 'Sex',
'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.',
'personalization_genders_modal_default' => 'Select the default gender for a new contact',
'personalization_genders_modal_delete' => 'حذف نوع الجنس',
'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?',
'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?',
'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?',
'personalization_genders_modal_error' => 'Please choose a gender from the list.',
'personalization_genders_list_contact_number' => '{count} contact|{count} contacts',
'personalization_genders_table_name' => 'Name',
'personalization_genders_table_sex' => 'Sex',
'personalization_genders_table_default' => 'Default',
'personalization_genders_default' => 'Default gender',
'personalization_genders_make_default' => 'Change default gender',
'personalization_genders_select_default' => 'Select default gender',
'personalization_genders_m' => 'Male',
'personalization_genders_f' => 'Female',
'personalization_genders_o' => 'Other',
'personalization_genders_u' => 'Unknown',
'personalization_genders_n' => 'None or not applicable',
'personalization_reminder_rule_save' => 'لقد تم حفظ التغيير',
'personalization_reminder_rule_title' => 'شروط التذكير',
'personalization_reminder_rule_line' => 'قبل {count} يوم | قبل {count} أيام',
'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.',
'personalization_module_save' => 'لقد تم حفظ التغيير',
'personalization_module_title' => 'الميزات',
'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.',
'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting <a href=":url">Settings > Subscription</a>.',
'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting <a href="{url}">Settings > Subscription</a>.',
'reminder_time_to_send' => 'Time of the day reminders will be sent',
'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on <span title="{dateTimeUtc}" class="reminder-info">{dateTime}</span>.',
'personalization_activity_type_category_title' => 'تصنيفات نوع النشاط',
'personalization_activity_type_category_add' => 'إضافة فئة نوع نشاط جديد',
'personalization_activity_type_category_table_name' => 'الاسم',
'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.',
'personalization_activity_type_category_table_actions' => 'إجراءات',
'personalization_activity_type_category_modal_add' => 'إضافة فئة نوع نشاط جديد',
'personalization_activity_type_category_modal_edit' => 'تحرير فئة نوع نشاط',
'personalization_activity_type_category_modal_question' => 'What should we name this new category?',
'personalization_activity_type_add_button' => 'أضف نوع نشاط جديد',
'personalization_activity_type_modal_add' => 'أضف نوع نشاط جديد',
'personalization_activity_type_modal_question' => 'What should we name this new activity type?',
'personalization_activity_type_modal_edit' => 'تحرير نوع نشاط',
'personalization_activity_type_category_modal_delete' => 'حذف فئة نوع نشاط',
'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.',
'personalization_activity_type_modal_delete' => 'حذف نوع نشاط',
'personalization_activity_type_modal_delete_desc' => 'هل أنت متأكد من حذف نوع هذا النشاط؟ الأنشطة التي تعود لهذه الفئة لن تتأثر بهذا الحذف.',
'personalization_activity_type_modal_delete_error' => 'لا نستطيع إيجاد نوع هذا النشاط.',
'personalization_activity_type_category_modal_delete_error' => 'لا نستطيع إيجاد تصنيف هذا النوع من الأنشطة.',
'personalization_life_event_category_title' => 'Life event categories',
'personalization_live_event_category_table_name' => 'Name',
'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.',
'personalization_live_event_category_table_actions' => 'Actions',
'personalization_life_event_type_add_button' => 'Add a new life event type',
'personalization_life_event_type_modal_add' => 'Add a new life event type',
'personalization_life_event_type_modal_question' => 'What should we name this new life event type?',
'personalization_life_event_type_modal_edit' => 'Edit a life event type',
'personalization_life_event_type_modal_delete' => 'Delete a life event type',
'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.',
'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.',
'personalization_life_event_category_work_education' => 'العمل و التعليم',
'personalization_life_event_category_family_relationships' => 'العائلة و العلاقات',
'personalization_life_event_category_home_living' => 'المنزل و العيش',
'personalization_life_event_category_travel_experiences' => 'السفر و الخبرات',
'personalization_life_event_category_health_wellness' => 'الصحة و العافية',
'personalization_life_event_type_new_job' => 'عمل جديد',
'personalization_life_event_type_retirement' => 'التقاعد',
'personalization_life_event_type_new_school' => 'مدرسة جديدة',
'personalization_life_event_type_study_abroad' => 'الدراسة في الخارج',
'personalization_life_event_type_volunteer_work' => 'عمل تطوعي',
'personalization_life_event_type_published_book_or_paper' => 'نشر كتاب أو مقال',
'personalization_life_event_type_military_service' => 'خدمة عسكرية',
'personalization_life_event_type_first_met' => 'أول لقاء',
'personalization_life_event_type_new_relationship' => 'علاقة جديدة',
'personalization_life_event_type_engagement' => 'الخطوبة',
'personalization_life_event_type_marriage' => 'الزواج',
'personalization_life_event_type_anniversary' => 'ذكرى',
'personalization_life_event_type_expecting_a_baby' => 'انتظار مولود',
'personalization_life_event_type_new_child' => 'طفل جديد',
'personalization_life_event_type_new_family_member' => 'فرد جديد في العائلة',
'personalization_life_event_type_new_pet' => 'حيوان أليف جديد',
'personalization_life_event_type_end_of_relationship' => 'نهاية علاقة',
'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one',
'personalization_life_event_type_moved' => 'إنتقال',
'personalization_life_event_type_bought_a_home' => 'شراء منزل',
'personalization_life_event_type_home_improvement' => 'ترميم المنزل',
'personalization_life_event_type_holidays' => 'العطل',
'personalization_life_event_type_new_vehicle' => 'مركبة جديدة',
'personalization_life_event_type_new_roommate' => 'رفيق سكن جديد',
'personalization_life_event_type_overcame_an_illness' => 'التغلب على المرض',
'personalization_life_event_type_quit_a_habit' => 'الإقلاع عن عادة',
'personalization_life_event_type_new_eating_habits' => 'عادات أكل جديدة',
'personalization_life_event_type_weight_loss' => 'فقدان الوزن',
'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts',
'personalization_life_event_type_broken_bone' => 'Broke a bone',
'personalization_life_event_type_removed_braces' => 'Had braces removed',
'personalization_life_event_type_surgery' => 'Had surgery',
'personalization_life_event_type_dentist' => 'Had dental treatment',
'personalization_life_event_type_new_sport' => 'Started playing a new sport',
'personalization_life_event_type_new_hobby' => 'Took up a new hobby',
'personalization_life_event_type_new_instrument' => 'Started learning a new instrument',
'personalization_life_event_type_new_language' => 'Started learning a new language',
'personalization_life_event_type_tattoo_or_piercing' => 'وشم أو ثقب',
'personalization_life_event_type_new_license' => 'رخصة جديدة',
'personalization_life_event_type_travel' => 'السفر',
'personalization_life_event_type_achievement_or_award' => 'إنجاز أو جائزة',
'personalization_life_event_type_changed_beliefs' => 'تغير المعتقدات',
'personalization_life_event_type_first_word' => 'أول كلمة',
'personalization_life_event_type_first_kiss' => 'First kiss',
'storage_title' => 'Storage',
'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).',
'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.',
'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.',
'dav_title' => 'WebDAV',
'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.',
'dav_copy_help' => 'Copy into your clipboard',
'dav_clipboard_copied' => 'Value copied into your clipboard',
'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:',
'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.',
'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.',
'dav_url_carddav' => 'CardDAV url for Contacts resource:',
'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:',
'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:',
'dav_title_carddav' => 'CardDAV',
'dav_title_caldav' => 'CalDAV',
'dav_carddav_export' => 'Export all contacts in one file',
'dav_caldav_birthdays_export' => 'Export all birthdays in one file',
'dav_caldav_tasks_export' => 'Export all tasks in one file',
'archive_title' => 'Archive all of the contacts in your account',
'archive_desc' => 'This will archive all of the contacts in your account.',
'archive_cta' => 'Archive all of your contacts',
'logs_title' => 'Everything that has happened to this account',
'logs_actor' => 'Actor',
'logs_timestamp' => 'Timestamp',
'logs_description' => 'Description',
'logs_subject' => 'Subject',
'logs_size' => 'Size (Kb)',
'logs_object' => 'Object',
];

View File

@@ -0,0 +1,166 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'يجب قبول :attribute',
'active_url' => ':attribute لا يُمثّل رابطًا صحيحًا',
'after' => 'يجب على :attribute أن يكون تاريخًا لاحقًا للتاريخ :date.',
'after_or_equal' => 'يجب أن يكون تاريخ :attribute بعد أو مساوياً لـ:date.',
'alpha' => 'يجب أن يحتوي :attribute فقط على أحرف.',
'alpha_dash' => 'يجب أن لا يحتوي :attribute سوى على حروف، أرقام ومطّات.',
'alpha_num' => 'يجب أن يحتوي :attribute فقط على أحرف و أرقام.',
'array' => 'يجب أن يكون :attribute مرتباً.',
'before' => 'يجب أن يكون تاريخ :attribute قبل :date.',
'before_or_equal' => 'يجب أن يكون تاريخ :attribute قبل أو مساوياً لـ:date.',
'between' => [
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
'file' => 'يجب أن يكون :attribute بين :min و :max كيلوبايت.',
'string' => 'يجب أن يكون :attribute بين :min و :max من الأحرف.',
'array' => 'يجب أن يكون لـ :attribute بين :min و :max من العناصر.',
],
'boolean' => 'حقل :attribute يجب أن يكون صحيحاً أو خاطئاً.',
'confirmed' => 'تأكيد :attribute غير متطابق.',
'date' => 'إن تاريخ :attribute غير صالح.',
'date_equals' => 'يجب أن يكون :attribute مطابقاً للتاريخ :date.',
'date_format' => 'إن :attribute غير متطابق مع تنسيق :format.',
'different' => 'إن :attribute و :other يجب أن يكونا مختلفين.',
'digits' => 'يجب أن يحتوي :attribute على :digits رقمًا/أرقام',
'digits_between' => 'يجب أن يحتوي :attribute بين :min و :max رقمًا/أرقام ',
'dimensions' => 'الـ :attribute يحتوي على أبعاد صورة غير صالحة.',
'distinct' => 'للحقل :attribute قيمة مُكرّرة.',
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صحيح البُنية',
'ends_with' => 'يجب أن ينتهي :attribute بأحد القيم التالية: :values',
'exists' => 'القيمة المحددة :attribute غير موجودة',
'file' => 'الـ :attribute يجب أن يكون ملفا.',
'filled' => ':attribute إجباري',
'gt' => [
'numeric' => 'يجب أن تكون قيمة :attribute أكبر من :value.',
'file' => 'يجب أن يكون حجم الملف :attribute أكبر من :value كيلوبايت.',
'string' => 'يجب أن يكون طول النّص :attribute أكثر من :value حروفٍ/حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أكثر من :value عناصر/عنصر.',
],
'gte' => [
'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أكبر من :value.',
'file' => 'يجب أن يكون حجم الملف :attribute على الأقل :value كيلوبايت.',
'string' => 'يجب أن يكون طول النص :attribute على الأقل :value حروفٍ/حرفًا.',
'array' => 'يجب أن يحتوي :attribute على الأقل على :value عُنصرًا/عناصر.',
],
'image' => 'يجب أن يكون :attribute صورةً',
'in' => ':attribute غير موجود',
'in_array' => ':attribute غير موجود في :other.',
'integer' => 'يجب أن يكون :attribute عددًا صحيحًا',
'ip' => 'يجب أن يكون :attribute عنوان IP صحيحًا',
'ipv4' => 'يجب أن يكون :attribute عنوان IPv4 صحيحًا.',
'ipv6' => 'يجب أن يكون :attribute عنوان IPv6 صحيحًا.',
'json' => 'يجب أن يكون :attribute نصآ من نوع JSON.',
'lt' => [
'numeric' => 'يجب أن تكون قيمة :attribute أصغر من :value.',
'file' => 'يجب أن يكون حجم الملف :attribute أصغر من :value كيلوبايت.',
'string' => 'يجب أن يكون طول النّص :attribute أقل من :value حروفٍ/حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أقل من :value عناصر/عنصر.',
],
'lte' => [
'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أصغر من :value.',
'file' => 'يجب أن لا يتجاوز حجم الملف :attribute :value كيلوبايت.',
'string' => 'يجب أن لا يتجاوز طول النّص :attribute :value حروفٍ/حرفًا.',
'array' => 'يجب أن لا يحتوي :attribute على أكثر من :value عناصر/عنصر.',
],
'max' => [
'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أصغر من :max.',
'file' => 'يجب أن لا يتجاوز حجم الملف :attribute :max كيلوبايت',
'string' => 'يجب أن لا يتجاوز طول النّص :attribute :max حروفٍ/حرفًا',
'array' => 'يجب أن لا يحتوي :attribute على أكثر من :max عناصر/عنصر.',
],
'mimes' => 'يجب أن يكون ملفًا من نوع : :values.',
'mimetypes' => 'يجب أن يكون ملفًا من نوع : :values.',
'min' => [
'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أكبر من :min.',
'file' => 'يجب أن يكون حجم الملف :attribute على الأقل :min كيلوبايت',
'string' => 'يجب أن يكون طول النص :attribute على الأقل :min حروفٍ/حرفًا',
'array' => 'يجب أن يحتوي :attribute على الأقل على :min عُنصرًا/عناصر',
],
'not_in' => ':attribute موجود',
'not_regex' => 'صيغة :attribute غير صحيحة.',
'numeric' => 'يجب أن يكون :attribute رقماً.',
'password' => 'كلمة المرور غير صحيحة.',
'present' => 'يجب تقديم :attribute',
'regex' => 'صيغة :attribute .غير صحيحة',
'required' => ':attribute مطلوب.',
'required_if' => ':attribute مطلوب في حال ما إذا كان :other يساوي :value.',
'required_unless' => ':attribute مطلوب في حال ما لم يكن :other يساوي :values.',
'required_with' => ':attribute مطلوب إذا توفّر :values.',
'required_with_all' => ':attribute مطلوب إذا توفّر :values.',
'required_without' => ':attribute مطلوب إذا لم يتوفّر :values.',
'required_without_all' => ':attribute مطلوب إذا لم يتوفّر :values.',
'same' => 'يجب أن يتطابق :attribute مع :other',
'size' => [
'numeric' => 'يجب أن تكون قيمة :attribute مساوية لـ :size',
'file' => 'يجب أن يكون حجم الملف :attribute :size كيلوبايت',
'string' => 'يجب أن يحتوي النص :attribute على :size حروفٍ/حرفًا بالضبط',
'array' => 'يجب أن يحتوي :attribute على :size عنصرٍ/عناصر بالضبط',
],
'starts_with' => 'يجب أن يبدأ :attribute بأحد القيم التالية: :values',
'string' => 'يجب أن يكون :attribute نصآ.',
'timezone' => 'يجب أن يكون :attribute نطاقًا زمنيًا صحيحًا',
'unique' => 'قيمة :attribute مُستخدمة من قبل',
'uploaded' => 'فشل في تحميل الـ :attribute',
'url' => 'صيغة الرابط :attribute غير صحيحة',
'uuid' => ':attribute يجب أن يكون بصيغة UUID سليمة.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
'vue' => [
'max' => [
'numeric' => '{field} may not be greater than {max}.',
'string' => '{field} may not be greater than {max} characters.',
],
'required' => '{field} is required.',
'url' => '{field} is not a valid URL.',
],
];

7
resources/lang/cs.json Normal file
View File

@@ -0,0 +1,7 @@
{
"The :attribute must contain at least one uppercase and one lowercase letter.": "Heslo musí obsahovat alespoň jedno velké písmeno a jedno malé písmeno.",
"The :attribute must contain at least one letter.": "Heslo musí obsahovat alespoň jedno písmeno.",
"The :attribute must contain at least one symbol.": "Heslo musí obsahovat alespoň jeden symbol.",
"The :attribute must contain at least one number.": "Heslo musí obsahovat alespoň jedno číslo.",
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Uvedené :attribute se objevilo v úniku dat. Zvolte prosím jiné :attribute."
}

571
resources/lang/cs/app.php Normal file
View File

@@ -0,0 +1,571 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'yes' => 'Ano',
'no' => 'Ne',
'update' => 'Aktualizovat',
'save' => 'Uložit',
'add' => 'Přidat',
'cancel' => 'Zrušit',
'confirm' => 'Potvrdit',
'delete_confirm' => 'Jste si jisti?',
'delete' => 'Smazat',
'edit' => 'Upravit',
'upload' => 'Nahrát',
'download' => 'Stáhnout',
'save_close' => 'Uložit a zavřít',
'close' => 'Zavřít',
'copy' => 'Kopírovat',
'create' => 'Vytvořit',
'remove' => 'Odstranit',
'revoke' => 'Odvolat',
'done' => 'Hotovo',
'back' => 'Zpět',
'verify' => 'Ověřit',
'new' => 'nový',
'unknown' => 'Nevím',
'load_more' => 'Načíst další',
'loading' => 'Načítání…',
'with' => 's',
'today' => 'dnes',
'yesterday' => 'včera',
'another_day' => 'jiný den',
'date' => 'Datum',
'type' => 'Typ',
'zoom' => 'Zvětšení',
'upgrade' => 'Upgradujte pro odemknutí',
'percent_uploaded' => '{percent}% nahráno',
'retry' => 'Opakovat',
'filter' => 'Filtrovat seznam',
'go_back' => 'Jít zpět',
'file_selected' => 'Jeden soubor vybrán…| vybráno {count} souborů…',
'application_title' => 'Monica – správce osobních vztahů',
'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.',
'application_og_title' => 'Pro lepší vztahy s vašimi blízkými. Bezplatný online CRM pro přátele a rodinu.',
'markdown_description' => 'Chcete pohodlně formátovat text? Podporujeme formát markdown pro značení tučně, kurzivou, vytváření seznamu a další.',
'markdown_link' => 'Číst dokumentaci',
'header_settings_link' => 'Nastavení',
'header_logout_link' => 'Odhlásit',
'header_changelog_link' => 'Změny produktu',
'main_nav_cta' => 'Přidat osobu',
'main_nav_dashboard' => 'Nástěnka',
'main_nav_family' => 'Kontakty',
'main_nav_journal' => 'Deník',
'main_nav_activities' => 'Aktivity',
'main_nav_tasks' => 'Úkoly',
'footer_remarks' => 'Komentáře?',
'footer_send_email' => 'Pošlete nám e-mail',
'footer_privacy' => 'Podmínky používání',
'footer_release' => 'Poznámky k vydání',
'footer_newsletter' => 'Odběr novinek',
'footer_source_code' => 'Monica na GitHubu',
'footer_version' => 'Verze: :version',
'footer_new_version' => 'Je dostupné nová verze aplikace Monica',
'footer_modal_version_whats_new' => 'Novinky',
'footer_modal_version_release_away' => 'Jste jedno vydání pozadu za nejnovější dostupnou verzí. Měli byste aktualizovat svou instanci.|Jste :number vydání pozadu za nejnovější dostupnou verzí. Měli byste aktualizovat svou instanci.',
'breadcrumb_dashboard' => 'Nástěnka',
'breadcrumb_list_contacts' => 'Seznam kontaktů',
'breadcrumb_archived_contacts' => 'Archivované kontakty',
'breadcrumb_journal' => 'Deník',
'breadcrumb_settings' => 'Nastavení',
'breadcrumb_settings_export' => 'Export',
'breadcrumb_settings_users' => 'Uživatelé',
'breadcrumb_settings_users_add' => 'Přidat uživatele',
'breadcrumb_settings_subscriptions' => 'Odběry',
'breadcrumb_settings_import' => 'Import',
'breadcrumb_settings_import_report' => 'Importovat report',
'breadcrumb_settings_import_upload' => 'Nahrát',
'breadcrumb_settings_tags' => 'Tagy',
'breadcrumb_add_significant_other' => 'Přidat drahou polovičku',
'breadcrumb_edit_significant_other' => 'Upravit drahou polovičku',
'breadcrumb_add_note' => 'Přidat poznámku',
'breadcrumb_edit_note' => 'Upravit poznámku',
'breadcrumb_api' => 'API',
'breadcrumb_dav' => 'DAV zdroje',
'breadcrumb_edit_introductions' => 'Jak jste se setkali',
'breadcrumb_settings_personalization' => 'Přizpůsobení',
'breadcrumb_settings_security' => 'Zabezpečení',
'breadcrumb_settings_security_2fa' => 'Dvoufázové ověření',
'breadcrumb_profile' => 'Profil :name',
'gender_male' => 'Muž',
'gender_female' => 'Žena',
'gender_none' => 'Nepovím',
'gender_no_gender' => 'Žádné pohlaví',
'error_title' => 'Jejda! Něco se pokazilo.',
'error_unauthorized' => 'Nemáte právo upravovat tento zdroj.',
'error_user_account' => 'Tento uživatel nepatří k danému účtu.',
'error_save' => 'Došlo k chybě při ukládání dat.',
'error_try_again' => 'Něco se pokazilo. Zkuste to znovu.',
'error_id' => 'ID chyby: :id',
'error_unavailable' => 'Služba není dostupná',
'error_maintenance' => 'Probíhá údržba. Budeme obratem zpátky.',
'error_help' => 'Budeme hned zpátky.',
'error_twitter' => 'Sledujte <a href="https://twitter.com/:twitter">náš Twitter účet</a> a buďte upozorněni, až služba znovu poběží.',
'error_no_term' => 'Pro tuto instanci zatím neexistují žádná pravidla.',
'default_save_success' => 'Data byla uložena.',
'compliance_title' => 'Omlouváme se za vyrušení.',
'compliance_desc' => 'Změnili jsme <a href=":urlterm" hreflang=":hreflang">Smluvní podmínky</a> a <a href=":url" hreflang=":hreflang">Zásady ochrany osobních údajů</a>. Podle zákona vás musíme požádat, abyste je zkontrolovali a přijali, abyste mohli nadále používat svůj účet.',
'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.',
'compliance_terms' => 'Přijmout nové podmínky a zásady ochrany osobních údajů',
// Relationship types
// Yes, each relationship type has 8 strings associated with it.
// This is because we need to indicate the name of the relationship type,
// and also the name of the opposite side of this relationship (father/son),
// and then, the feminine version of the string. Finally, in some sentences
// in the UI, we need to include the name of the person we add the relationship
// to.
'relationship_type_group_love' => 'Milostné vztahy',
'relationship_type_group_family' => 'Rodinné vztahy',
'relationship_type_group_friend' => 'Vztahy s přáteli',
'relationship_type_group_work' => 'Pracovní vztahy',
'relationship_type_group_other' => 'Jiný druh vztahů',
'relationship_type_partner' => 'drahá polovička',
'relationship_type_partner_female' => 'drahá polovička',
'relationship_type_partner_male' => 'drahá polovička',
'relationship_type_partner_with_name' => ':name’s significant other',
'relationship_type_partner_female_with_name' => ':name’s significant other',
'relationship_type_partner_male_with_name' => ':name’s significant other',
'relationship_type_spouse' => 'Manžel(ka)',
'relationship_type_spouse_female' => 'manželka',
'relationship_type_spouse_male' => 'manžel',
'relationship_type_spouse_with_name' => ':name’s spouse',
'relationship_type_spouse_female_with_name' => ':name’s wife',
'relationship_type_spouse_male_with_name' => ':name’s husband',
'relationship_type_date' => 'date',
'relationship_type_date_female' => 'date',
'relationship_type_date_male' => 'date',
'relationship_type_date_with_name' => ':name’s date',
'relationship_type_date_female_with_name' => ':name’s date',
'relationship_type_date_male_with_name' => ':name’s date',
'relationship_type_lover' => 'lover',
'relationship_type_lover_female' => 'lover',
'relationship_type_lover_male' => 'lover',
'relationship_type_lover_with_name' => ':name’s lover',
'relationship_type_lover_female_with_name' => ':name’s lover',
'relationship_type_lover_male_with_name' => ':name’s lover',
'relationship_type_inlovewith' => 'in love with',
'relationship_type_inlovewith_female' => 'in love with',
'relationship_type_inlovewith_male' => 'in love with',
'relationship_type_inlovewith_with_name' => 'someone :name is in love with',
'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with',
'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with',
'relationship_type_lovedby' => 'loved by',
'relationship_type_lovedby_female' => 'loved by',
'relationship_type_lovedby_male' => 'loved by',
'relationship_type_lovedby_with_name' => ':name’s secret lover',
'relationship_type_lovedby_female_with_name' => ':name’s secret lover',
'relationship_type_lovedby_male_with_name' => ':name’s secret lover',
'relationship_type_ex' => 'bývalý partner',
'relationship_type_ex_female' => 'bývalá přítelkyně',
'relationship_type_ex_male' => 'bývalý přítel',
'relationship_type_ex_with_name' => ':name’s ex-partner',
'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend',
'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend',
'relationship_type_parent' => 'rodič',
'relationship_type_parent_female' => 'matka',
'relationship_type_parent_male' => 'otec',
'relationship_type_parent_with_name' => ':name’s parent',
'relationship_type_parent_female_with_name' => ':name’s mother',
'relationship_type_parent_male_with_name' => ':name’s father',
'relationship_type_child' => 'dítě',
'relationship_type_child_female' => 'dcera',
'relationship_type_child_male' => 'syn',
'relationship_type_child_with_name' => ':name’s child',
'relationship_type_child_female_with_name' => ':name’s daughter',
'relationship_type_child_male_with_name' => ':name’s son',
'relationship_type_stepparent' => 'step-parent',
'relationship_type_stepparent_female' => 'stepmother',
'relationship_type_stepparent_male' => 'stepfather',
'relationship_type_stepparent_with_name' => ':name’s step-parent',
'relationship_type_stepparent_female_with_name' => ':name’s stepmother',
'relationship_type_stepparent_male_with_name' => ':name’s stepfather',
'relationship_type_stepchild' => 'stepchild',
'relationship_type_stepchild_female' => 'stepdaughter',
'relationship_type_stepchild_male' => 'stepson',
'relationship_type_stepchild_with_name' => ':name’s stepchild',
'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter',
'relationship_type_stepchild_male_with_name' => ':name’s stepson',
'relationship_type_sibling' => 'sourozenec',
'relationship_type_sibling_female' => 'sestra',
'relationship_type_sibling_male' => 'bratr',
'relationship_type_sibling_with_name' => ':name’s sibling',
'relationship_type_sibling_female_with_name' => ':name’s sister',
'relationship_type_sibling_male_with_name' => ':name’s brother',
'relationship_type_grandparent' => 'prarodič',
'relationship_type_grandparent_female' => 'babička',
'relationship_type_grandparent_male' => 'dědeček',
'relationship_type_grandparent_with_name' => ':name’s grandparent',
'relationship_type_grandparent_female_with_name' => ':name’s grandmother',
'relationship_type_grandparent_male_with_name' => ':name’s grandfather',
'relationship_type_grandchild' => 'vnouče',
'relationship_type_grandchild_female' => 'vnučka',
'relationship_type_grandchild_male' => 'vnuk',
'relationship_type_grandchild_with_name' => ':name’s grandchild',
'relationship_type_grandchild_female_with_name' => ':name’s granddauther',
'relationship_type_grandchild_male_with_name' => ':name’s grandson',
'relationship_type_uncle' => 'strýc',
'relationship_type_uncle_female' => 'teta',
'relationship_type_uncle_male' => 'strýc',
'relationship_type_uncle_with_name' => ':name’s uncle',
'relationship_type_uncle_female_with_name' => ':name’s aunt',
'relationship_type_uncle_male_with_name' => ':name’s uncle',
'relationship_type_nephew' => 'synovec',
'relationship_type_nephew_female' => 'neteř',
'relationship_type_nephew_male' => 'synovec',
'relationship_type_nephew_with_name' => ':name’s nephew',
'relationship_type_nephew_female_with_name' => ':name’s niece',
'relationship_type_nephew_male_with_name' => ':name’s nephew',
'relationship_type_cousin' => 'bratranec',
'relationship_type_cousin_female' => 'sestřenice',
'relationship_type_cousin_male' => 'bratranec',
'relationship_type_cousin_with_name' => ':name’s cousin',
'relationship_type_cousin_female_with_name' => ':name’s cousin',
'relationship_type_cousin_male_with_name' => ':name’s cousin',
'relationship_type_godfather' => 'godparent',
'relationship_type_godfather_female' => 'kmotra',
'relationship_type_godfather_male' => 'kmotr',
'relationship_type_godfather_with_name' => ':name’s godparent',
'relationship_type_godfather_female_with_name' => ':name’s godmother',
'relationship_type_godfather_male_with_name' => ':name’s godfather',
'relationship_type_godson' => 'godchild',
'relationship_type_godson_female' => 'goddaughter',
'relationship_type_godson_male' => 'godson',
'relationship_type_godson_with_name' => ':name’s godchild',
'relationship_type_godson_female_with_name' => ':name’s goddaughter',
'relationship_type_godson_male_with_name' => ':name’s godson',
'relationship_type_friend' => 'kamarád',
'relationship_type_friend_female' => 'kamarádka',
'relationship_type_friend_male' => 'přítel',
'relationship_type_friend_with_name' => ':name’s friend',
'relationship_type_friend_female_with_name' => ':name’s friend',
'relationship_type_friend_male_with_name' => ':name’s friend',
'relationship_type_bestfriend' => 'nejlepší kamarád',
'relationship_type_bestfriend_female' => 'nejlepší kamarádka',
'relationship_type_bestfriend_male' => 'nejlepší přítel',
'relationship_type_bestfriend_with_name' => ':name’s best friend',
'relationship_type_bestfriend_female_with_name' => ':name’s best friend',
'relationship_type_bestfriend_male_with_name' => ':name’s best friend',
'relationship_type_colleague' => 'kolega',
'relationship_type_colleague_female' => 'kolegyně',
'relationship_type_colleague_male' => 'colleague',
'relationship_type_colleague_with_name' => ':name’s colleague',
'relationship_type_colleague_female_with_name' => ':name’s colleague',
'relationship_type_colleague_male_with_name' => ':name’s colleague',
'relationship_type_boss' => 'nadřízený',
'relationship_type_boss_female' => 'nadřízená',
'relationship_type_boss_male' => 'boss',
'relationship_type_boss_with_name' => ':name’s boss',
'relationship_type_boss_female_with_name' => ':name’s boss',
'relationship_type_boss_male_with_name' => ':name’s boss',
'relationship_type_subordinate' => 'podřízený',
'relationship_type_subordinate_female' => 'podřízená',
'relationship_type_subordinate_male' => 'subordinate',
'relationship_type_subordinate_with_name' => ':name’s subordinate',
'relationship_type_subordinate_female_with_name' => ':name’s subordinate',
'relationship_type_subordinate_male_with_name' => ':name’s subordinate',
'relationship_type_mentor' => 'mentor',
'relationship_type_mentor_female' => 'mentor',
'relationship_type_mentor_male' => 'mentor',
'relationship_type_mentor_with_name' => ':name’s mentor',
'relationship_type_mentor_female_with_name' => ':name’s mentor',
'relationship_type_mentor_male_with_name' => ':name’s mentor',
'relationship_type_protege' => 'protégé',
'relationship_type_protege_female' => 'protégé',
'relationship_type_protege_male' => 'protégé',
'relationship_type_protege_with_name' => ':name’s protégé',
'relationship_type_protege_female_with_name' => ':name’s protégé',
'relationship_type_protege_male_with_name' => ':name’s protégé',
'relationship_type_ex_husband' => 'bývalý manžel/ka',
'relationship_type_ex_husband_female' => 'ex wife',
'relationship_type_ex_husband_male' => 'bývalý manžel',
'relationship_type_ex_husband_with_name' => ':name’s ex-spouse',
'relationship_type_ex_husband_female_with_name' => ':name’s ex wife',
'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband',
// emotions
'emotion_primary_love' => 'Láska',
'emotion_primary_joy' => 'Radost',
'emotion_primary_surprise' => 'Překvapení',
'emotion_primary_anger' => 'Hněv',
'emotion_primary_sadness' => 'Smutek',
'emotion_primary_fear' => 'Strach',
'emotion_secondary_affection' => 'Náklonnost',
'emotion_secondary_lust' => 'Lust',
'emotion_secondary_longing' => 'Longing',
'emotion_secondary_cheerfulness' => 'Cheerfulness',
'emotion_secondary_zest' => 'Zest',
'emotion_secondary_contentment' => 'Contentment',
'emotion_secondary_pride' => 'Hrdost',
'emotion_secondary_optimism' => 'Optimismus',
'emotion_secondary_enthrallment' => 'Enthrallment',
'emotion_secondary_relief' => 'Úleva',
'emotion_secondary_surprise' => 'Překvapení',
'emotion_secondary_irritation' => 'Podrážděnost',
'emotion_secondary_exasperation' => 'Exasperation',
'emotion_secondary_rage' => 'Vztek',
'emotion_secondary_disgust' => 'Znechucení',
'emotion_secondary_envy' => 'Envy',
'emotion_secondary_suffering' => 'Suffering',
'emotion_secondary_sadness' => 'Smutek',
'emotion_secondary_disappointment' => 'Zklamání',
'emotion_secondary_shame' => 'Shame',
'emotion_secondary_neglect' => 'Neglect',
'emotion_secondary_sympathy' => 'Sympatie',
'emotion_secondary_horror' => 'Horror',
'emotion_secondary_nervousness' => 'Nervozita',
'emotion_adoration' => 'Adoration',
'emotion_affection' => 'Náklonnost',
'emotion_love' => 'Láska',
'emotion_fondness' => 'Fondness',
'emotion_liking' => 'Liking',
'emotion_attraction' => 'Attraction',
'emotion_caring' => 'Caring',
'emotion_tenderness' => 'Tenderness',
'emotion_compassion' => 'Compassion',
'emotion_sentimentality' => 'Sentimentality',
'emotion_arousal' => 'Arousal',
'emotion_desire' => 'Desire',
'emotion_lust' => 'Lust',
'emotion_passion' => 'Passion',
'emotion_infatuation' => 'Infatuation',
'emotion_longing' => 'Longing',
'emotion_amusement' => 'Amusement',
'emotion_bliss' => 'Bliss',
'emotion_cheerfulness' => 'Cheerfulness',
'emotion_gaiety' => 'Gaiety',
'emotion_glee' => 'Glee',
'emotion_jolliness' => 'Jolliness',
'emotion_joviality' => 'Joviality',
'emotion_joy' => 'Joy',
'emotion_delight' => 'Delight',
'emotion_enjoyment' => 'Enjoyment',
'emotion_gladness' => 'Gladness',
'emotion_happiness' => 'Happiness',
'emotion_jubilation' => 'Jubilation',
'emotion_elation' => 'Elation',
'emotion_satisfaction' => 'Satisfaction',
'emotion_ecstasy' => 'Ecstasy',
'emotion_euphoria' => 'Euphoria',
'emotion_enthusiasm' => 'Enthusiasm',
'emotion_zeal' => 'Zeal',
'emotion_zest' => 'Zest',
'emotion_excitement' => 'Excitement',
'emotion_thrill' => 'Thrill',
'emotion_exhilaration' => 'Exhilaration',
'emotion_contentment' => 'Contentment',
'emotion_pleasure' => 'Pleasure',
'emotion_pride' => 'Pride',
'emotion_eagerness' => 'Eagerness',
'emotion_hope' => 'Hope',
'emotion_optimism' => 'Optimism',
'emotion_enthrallment' => 'Enthrallment',
'emotion_rapture' => 'Rapture',
'emotion_relief' => 'Relief',
'emotion_amazement' => 'Amazement',
'emotion_surprise' => 'Surprise',
'emotion_astonishment' => 'Astonishment',
'emotion_aggravation' => 'Aggravation',
'emotion_irritation' => 'Irritation',
'emotion_agitation' => 'Agitation',
'emotion_annoyance' => 'Annoyance',
'emotion_grouchiness' => 'Grouchiness',
'emotion_grumpiness' => 'Grumpiness',
'emotion_exasperation' => 'Exasperation',
'emotion_frustration' => 'Frustration',
'emotion_anger' => 'Anger',
'emotion_rage' => 'Rage',
'emotion_outrage' => 'Outrage',
'emotion_fury' => 'Fury',
'emotion_wrath' => 'Wrath',
'emotion_hostility' => 'Hostility',
'emotion_ferocity' => 'Ferocity',
'emotion_bitterness' => 'Bitterness',
'emotion_hate' => 'Hate',
'emotion_loathing' => 'Loathing',
'emotion_scorn' => 'Scorn',
'emotion_spite' => 'Spite',
'emotion_vengefulness' => 'Vengefulness',
'emotion_dislike' => 'Dislike',
'emotion_resentment' => 'Resentment',
'emotion_disgust' => 'Disgust',
'emotion_revulsion' => 'Revulsion',
'emotion_contempt' => 'Contempt',
'emotion_envy' => 'Envy',
'emotion_jealousy' => 'Jealousy',
'emotion_agony' => 'Agony',
'emotion_suffering' => 'Suffering',
'emotion_hurt' => 'Hurt',
'emotion_anguish' => 'Anguish',
'emotion_depression' => 'Depression',
'emotion_despair' => 'Despair',
'emotion_hopelessness' => 'Hopelessness',
'emotion_gloom' => 'Gloom',
'emotion_glumness' => 'Glumness',
'emotion_sadness' => 'Sadness',
'emotion_unhappiness' => 'Unhappiness',
'emotion_grief' => 'Grief',
'emotion_sorrow' => 'Sorrow',
'emotion_woe' => 'Woe',
'emotion_misery' => 'Misery',
'emotion_melancholy' => 'Melancholy',
'emotion_dismay' => 'Dismay',
'emotion_disappointment' => 'Disappointment',
'emotion_displeasure' => 'Displeasure',
'emotion_guilt' => 'Guilt',
'emotion_shame' => 'Shame',
'emotion_regret' => 'Regret',
'emotion_remorse' => 'Remorse',
'emotion_alienation' => 'Alienation',
'emotion_isolation' => 'Isolation',
'emotion_neglect' => 'Neglect',
'emotion_loneliness' => 'Loneliness',
'emotion_rejection' => 'Rejection',
'emotion_homesickness' => 'Homesickness',
'emotion_defeat' => 'Defeat',
'emotion_dejection' => 'Dejection',
'emotion_insecurity' => 'Insecurity',
'emotion_embarrassment' => 'Embarrassment',
'emotion_humiliation' => 'Humiliation',
'emotion_insult' => 'Insult',
'emotion_pity' => 'Pity',
'emotion_sympathy' => 'Sympathy',
'emotion_alarm' => 'Alarm',
'emotion_shock' => 'Shock',
'emotion_fear' => 'Fear',
'emotion_fright' => 'Fright',
'emotion_horror' => 'Horror',
'emotion_terror' => 'Terror',
'emotion_panic' => 'Panic',
'emotion_hysteria' => 'Hysteria',
'emotion_mortification' => 'Mortification',
'emotion_anxiety' => 'Anxiety',
'emotion_nervousness' => 'Nervousness',
'emotion_tenseness' => 'Tenseness',
'emotion_uneasiness' => 'Uneasiness',
'emotion_apprehension' => 'Apprehension',
'emotion_worry' => 'Worry',
'emotion_distress' => 'Distress',
'emotion_dread' => 'Dread',
// weather
'weather_sunny' => 'Sunny',
'weather_clear' => 'Clear',
'weather_clear-day' => 'Clear',
'weather_clear-night' => 'Jasná noc',
'weather_light-drizzle' => 'Slabé mrholení',
'weather_patchy-light-drizzle' => 'Místy slabé mrholení',
'weather_patchy-light-rain' => 'Místy slabý déšť',
'weather_light-rain' => 'Slabý déšť',
'weather_moderate-rain-at-times' => 'Občasný mírný déšť',
'weather_moderate-rain' => 'Mírný déšť',
'weather_patchy-rain-possible' => 'Místy možný déšť',
'weather_heavy-rain-at-times' => 'Občasný vydatný déšť',
'weather_heavy-rain' => 'Vydatný déšť',
'weather_light-freezing-rain' => 'Slabý mrznoucí déšť',
'weather_moderate-or-heavy-freezing-rain' => 'Střední nebo silný mrznoucí déšť',
'weather_light-sleet' => 'Slabý déšť se sněhem',
'weather_moderate-or-heavy-rain-shower' => 'Střední nebo silné déšťové přeháňky',
'weather_light-rain-shower' => 'Slabé dešťové přeháňky',
'weather_torrential-rain-shower' => 'Přívalové dešťové přeháňky',
'weather_rain' => 'Déšť',
'weather_snow' => 'Sněžení',
'weather_blowing-snow' => 'Zvířený sníh',
'weather_patchy-light-snow' => 'Místy slabé sněžení',
'weather_light-snow' => 'Slabé sněžení',
'weather_patchy-moderate-snow' => 'Místy střední sněžení',
'weather_moderate-snow' => 'Mírné sněžení',
'weather_patchy-heavy-snow' => 'Místy silné sněžení',
'weather_heavy-snow' => 'Silné sněžení',
'weather_light-snow-showers' => 'Slabé sněhové přeháňky',
'weather_moderate-or-heavy-snow-showers' => 'Střední nebo silné sněhové přeháňky',
'weather_patchy-snow-possible' => 'Místy možné sněžení',
'weather_patchy-sleet-possible' => 'Místy možný déšť se sněhem',
'weather_moderate-or-heavy-sleet' => 'Střední nebo silný déšť se sněhem',
'weather_light-sleet-showers' => 'Slabé přeháňky deště se sněhem',
'weather_moderate-or-heavy-sleet-showers' => 'Střední nebo silné přeháňky deště se sněhem',
'weather_sleet' => 'Déšť se sněhem',
'weather_wind' => 'Vítr',
'weather_fog' => 'Mlha',
'weather_freezing-fog' => 'Mrznoucí mlha',
'weather_mist' => 'Opar',
'weather_blizzard' => 'Sněhová vánice',
'weather_overcast' => 'Zataženo',
'weather_cloudy' => 'Oblačno',
'weather_partly-cloudy-day' => 'Částečně oblačno',
'weather_partly-cloudy-night' => 'Částečně oblačno',
'weather_freezing-drizzle' => 'Mrznoucí mrholení',
'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle',
'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible',
'weather_ice-pellets' => 'Ice pellets',
'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets',
'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets',
'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible',
'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder',
'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder',
'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder',
'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder',
'weather_current_temperature_celsius' => ':temperature °C',
'weather_current_temperature_fahrenheit' => ':temperature °F',
'weather_current_title' => 'Current weather',
// dav
'dav_contacts' => 'Contacts',
'dav_contacts_description' => ':name’s contacts',
'dav_birthdays' => 'Birthdays',
'dav_birthdays_description' => ':name’s contact’s birthdays',
'dav_tasks' => 'Tasks',
'dav_tasks_description' => ':name’s tasks',
// contact list
'contact_list_avatar' => 'Avatar',
'contact_list_name' => 'Contact',
'contact_list_description' => 'Description',
];

View File

@@ -0,0 +1,89 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Tyto přihlašovací údaje neodpovídají žadnému záznamu.',
'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds vteřin.',
'not_authorized' => 'Nejste oprávněni provést tuto akci',
'signup_disabled' => 'Nové registrace jsou aktuálně zastaveny',
'signup_error' => 'An error occured trying to register the user',
'back_homepage' => 'Zpět na domovskou stránku',
'mfa_auth_otp' => 'Authenticate with your two factor device',
'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)',
'2fa_title' => 'Two Factor Authentication',
'2fa_wrong_validation' => 'The two factor authentication has failed.',
'2fa_one_time_password' => 'Two factor authentication code',
'2fa_recuperation_code' => 'Enter a two factor recovery code',
'2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code',
'2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code',
'login_to_account' => 'Login to your account',
'login_with_recovery' => 'Login with a recovery code',
'login_again' => 'Please login again to your account',
'email' => 'Email',
'password' => 'Password',
'recovery' => 'Recovery code',
'login' => 'Login',
'button_remember' => 'Remember Me',
'password_forget' => 'Forget your password?',
'password_reset' => 'Reset your password',
'use_recovery' => 'Or you can use a <a href=":url">recovery code</a>',
'signup_no_account' => 'Don’t have an account?',
'signup' => 'Registrace',
'create_account' => 'Vytvořte první účet <a href=":url">registrací</a>',
'change_language_title' => 'Změnit jazyk:',
'change_language' => 'Změnit jazyk na :lang',
'password_reset_title' => 'Resetovat heslo',
'password_reset_email' => 'E-mailová adresa',
'password_reset_send_link' => 'Odeslat odkaz na obnovení hesla',
'password_reset_password' => 'Heslo',
'password_reset_password_confirm' => 'Potvrdit heslo',
'password_reset_action' => 'Resetovat heslo',
'password_reset_email_content' => 'Pro zresetování hesla klikněte na:',
'register_title_welcome' => 'Vítejte v nově nainstalované instanci Monica',
'register_create_account' => 'Abyste mohli používat aplikaci Monica je třeba založit účet',
'register_title_create' => 'Založte svůj účet Monica',
'register_login' => '<a href=":url">Přihlaste se</a>, pokud již máte účet.',
'register_email' => 'Zadejte platnou e-mailovou adresu',
'register_email_example' => 'ucet@domena',
'register_firstname' => 'Jméno',
'register_firstname_example' => 'např. Jan',
'register_lastname' => 'Příjmení',
'register_lastname_example' => 'např. Novák',
'register_password' => 'Heslo',
'register_password_example' => 'Zadejte bezpečné heslo',
'register_password_confirmation' => 'Potvrzení hesla',
'register_action' => 'Registrovat',
'register_policy' => 'Signing up signifies you’ve read and agree to our <a href=":url" hreflang=":hreflang">Privacy Policy</a> and <a href=":urlterm" hreflang=":hreflang">Terms of use</a>.',
'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.',
'confirmation_title' => 'Verify Your Email Address',
'confirmation_fresh' => 'A fresh verification link has been sent to your email address.',
'confirmation_check' => 'Before proceeding, please check your email for a verification link.',
'confirmation_request_another' => 'If you did not receive the email <a :action>click here to request another</a>.',
'confirmation_again' => 'If you want to change your email address you can <a href=":url" class="alert-link">click here</a>.',
'email_change_current_email' => 'Aktuální e-mailová adresa:',
'email_change_title' => 'Změna e-mailové adresy',
'email_change_new' => 'Nová e-mailová adresa',
'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.',
];

View File

@@ -0,0 +1,12 @@
<?php
/**
* ⚠️ Editing not allowed except for 'en' language.
*
* @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations.
*/
return [
'title' => 'Product changes',
'note' => 'Note: unfortunately, this page is only in English.',
];

Some files were not shown because too many files have changed in this diff Show More