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