ldc-crm/resources/ts/bo/components/ClaimConversationComponent.vue
2025-10-07 10:32:38 +02:00

263 lines
7.3 KiB
Vue

<template>
<div
class="d-flex flex-column justify-content-between align-items-center border shadow-sm rounded-3 p-3 h-100"
ref="claimConversation"
>
<div
ref="messagesWrapper"
class="col-12 claim-messages-wrapper overflow-scroll pe-3"
>
<template v-if="claimMessageModels.length">
<template
v-for="(claimMessageModel, claimMessageModelIndex) in claimMessageModels"
:key="claimMessageModelIndex"
>
<span
v-if="shouldDisplayDate(claimMessageModelIndex)"
:class="`date-separator d-flex flex-row justify-content-between align-items-center text-secondary fst-italic w-100 ${claimMessageModelIndex === 0 ? 'mb-4' : 'my-4'}`"
>
{{ formatDate(String(claimMessageModel.created_at)) }}
</span>
<ClaimMessageComponent
:claim-message-model="claimMessageModel"
:auth-user-id="authUserId"
:loading="loading"
@set-loading-message="setLoadingMessage"
/>
</template>
</template>
<div
v-else
class="d-flex flex-column justify-content-center align-items-center text-muted fst-italic h-100"
>
<FontAwesomeIcon
icon="fa-solid fa-comment-slash"
size="2xl"
/>
<p class="my-3">
{{ trans.methods.__('Aucune activité enregistrée à ce jour') }}
</p>
</div>
</div>
<div class="col-12 mt-3">
<div class="d-flex justify-content-center align-items-start">
<textarea
class="form-control"
:class="{'is-invalid': errors.methods.parseValidationInput('content', allErrors)}"
:placeholder="trans.methods.__('Votre message...')"
name="content"
v-model="content"
:disabled="loading || loadingMessage"
/>
<button
class="btn btn-primary ms-1"
data-bs-tooltip="tooltip"
type="button"
:title="trans.methods.__('Envoyer votre message')"
@click="storeMessage()"
:disabled="loading || loadingMessage"
>
<div
v-show="loading"
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">{{ trans.methods.__('Chargement...') }}</span>
</div>
<div v-show="!loading">
<FontAwesomeIcon icon="fa-solid fa-paper-plane" />
</div>
</button>
</div>
<small class="text-danger m-0">
{{ errors.methods.parseValidationInput('content', allErrors) }}
</small>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, nextTick, useAttrs, watch } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import type { PropType } from "vue";
import trans from "./../../modules/trans";
import route from "./../../modules/route";
import { Tooltips } from "./../../modules/tooltips";
import errors from "./../../modules/errors";
import ClaimMessageComponent from "./ClaimMessageComponent.vue";
defineOptions({
name: "ClaimConversationComponent"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * PROPS
const props = defineProps({
authUserId: {
type: Number,
default: Math.pow(10, 16) / Math.random(),
},
claimModel: {
type: Object as PropType<LaravelModel & LaravelModelClaimRelations>,
default: () => { return {}; },
},
claimMessageModels: {
type: Object as PropType<Array<LaravelModel & LaravelModelUserRelation>>,
default: () => { return []; },
},
loading: {
type: Boolean,
default: false,
},
loadingMessage: {
type: Boolean,
default: false,
},
allErrors: {
type: Object as PropType<LaravelValidationErrors>,
default: () => { return {}; },
},
});
// * REFS
const claimConversation = ref<HTMLDivElement|null>(null);
const messagesWrapper = ref<HTMLDivElement|null>(null);
// * DATA
const authUserId = ref<number>(props.authUserId);
const claimModel = ref<LaravelModel>(props.claimModel);
const claimMessageModels = ref<Array<LaravelModel & LaravelModelUserRelation>>(props.claimMessageModels);
const content = ref<string>("");
const loading = ref<boolean>(props.loading);
const loadingMessage = ref<boolean>(props.loadingMessage);
const tooltips = ref<Tooltips|null>(null);
const allErrors = ref<LaravelValidationErrors>(props.allErrors);
// * COMPUTED
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
// * LIFECYCLE
onMounted((): void => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
if (!isUsedWithProps.value) {
authUserId.value = data.authUserId;
claimModel.value = data.claimModel;
claimMessageModels.value = data.claimMessageModels;
allErrors.value = data.allErrors;
}
initTooltips();
nextTick(() => {
scrollToBottom();
});
});
// * WATCHERS
watch(props, () => {
claimMessageModels.value = props.claimMessageModels;
nextTick(() => {
scrollToBottom();
});
});
// * METHODS
/**
* Store a new message.
* @return void
*/
function storeMessage(): void {
loading.value = true;
allErrors.value = {};
window.axios
.post(route.methods.getRoute("bo.claim_messages.store"), {
claim_id: claimModel.value.id,
content: content.value,
})
.then((reponse) => {
loading.value = false;
claimMessageModels.value = reponse.data;
content.value = "";
nextTick(() => {
scrollToBottom();
});
})
.catch((e) => {
loading.value = false;
allErrors.value = e.response?.data?.errors ?? {};
errors.methods.ajaxErrorHandler(e);
});
}
/**
* Scroll the wrapper message to the bottom.
* @return void
*/
function scrollToBottom(): void {
if (messagesWrapper.value)
messagesWrapper.value.scrollTop = messagesWrapper.value?.scrollHeight;
}
/**
* Set the loading value component correlated to the loading message component.
* @param boolean eventLoadingMessage
* @return void
*/
function setLoadingMessage(eventLoadingMessage: boolean): void {
loadingMessage.value = eventLoadingMessage;
}
/**
* Return a formated date.
* @param string unformatedDate
* @return void
*/
function formatDate(unformatedDate: string): string {
const date = new Date(unformatedDate);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
});
}
/**
* Set the visibility of the date separator.
* @param number index
*/
function shouldDisplayDate(index: number): boolean {
if (index === 0) return true;
const current = new Date(String(claimMessageModels.value[index].created_at)).toDateString();
const previous = new Date(String(claimMessageModels.value[index - 1].created_at)).toDateString();
return current !== previous;
}
/** Initialise all tooltips in the component. */
function initTooltips(): void {
setTimeout(() => {
tooltips.value = Tooltips.make({
type: "dom",
elements: claimConversation.value!.querySelectorAll("[data-bs-tooltip=\"tooltip\"]")
});
}, 500);
}
</script>
<style lang="scss" scopped>
.claim-messages-wrapper {
height: 24.6rem;
}
.date-separator {
&::before,
&::after {
content: "";
flex: 1;
border-bottom: 1px solid #dee2e6;
margin: 0 0.5rem;
width: 100%;
display: block;
}
}
</style>