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

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

View File

@@ -0,0 +1,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>