Merge branch 'develop' into fix/cw-4085
This commit is contained in:
@@ -1253,7 +1253,7 @@ export default {
|
||||
@apply relative py-0 px-4 -mt-px;
|
||||
|
||||
textarea {
|
||||
@apply shadow-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
|
||||
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
<woot-input
|
||||
v-model="v$.toEmailsVal.$model"
|
||||
type="text"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
:class="{ error: v$.toEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@blur="onBlur"
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
<div class="flex-1 min-w-0 m-0 rounded-none whitespace-nowrap">
|
||||
<woot-input
|
||||
v-model="v$.ccEmailsVal.$model"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
type="text"
|
||||
:class="{ error: v$.ccEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@@ -142,7 +142,7 @@ export default {
|
||||
<woot-input
|
||||
v-model="v$.bccEmailsVal.$model"
|
||||
type="text"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
|
||||
:class="{ error: v$.bccEmailsVal.$error }"
|
||||
:placeholder="
|
||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
order: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formatDate = dateString => {
|
||||
return format(new Date(dateString), 'MMM d, yyyy');
|
||||
};
|
||||
|
||||
const formatCurrency = (amount, currency) => {
|
||||
return new Intl.NumberFormat('en', {
|
||||
style: 'currency',
|
||||
currency: currency || 'USD',
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getStatusClass = status => {
|
||||
const classes = {
|
||||
paid: 'bg-n-teal-5 text-n-teal-12',
|
||||
};
|
||||
return classes[status] || 'bg-slate-50 text-slate-700';
|
||||
};
|
||||
|
||||
const getStatusI18nKey = (type, status = '') => {
|
||||
return `CONVERSATION_SIDEBAR.SHOPIFY.${type.toUpperCase()}_STATUS.${status.toUpperCase()}`;
|
||||
};
|
||||
|
||||
const fulfillmentStatus = computed(() => {
|
||||
const { fulfillment_status: status } = props.order;
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
return t(getStatusI18nKey('FULFILLMENT', status));
|
||||
});
|
||||
|
||||
const financialStatus = computed(() => {
|
||||
const { financial_status: status } = props.order;
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
return t(getStatusI18nKey('FINANCIAL', status));
|
||||
});
|
||||
|
||||
const getFulfillmentClass = status => {
|
||||
const classes = {
|
||||
fulfilled: 'text-green-600',
|
||||
partial: 'text-yellow-600',
|
||||
unfulfilled: 'text-red-600',
|
||||
};
|
||||
return classes[status] || 'text-slate-600';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="py-3 border-b border-n-weak last:border-b-0 flex flex-col gap-1.5"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium flex">
|
||||
<a
|
||||
:href="order.admin_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:underline text-n-slate-12 cursor-pointer truncate"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.ORDER_ID', { id: order.id }) }}
|
||||
<i class="i-lucide-external-link pl-5" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
:class="getStatusClass(order.financial_status)"
|
||||
class="text-xs px-2 py-1 rounded capitalize truncate"
|
||||
:title="financialStatus"
|
||||
>
|
||||
{{ financialStatus }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-n-slate-12">
|
||||
<span class="text-n-slate-11 border-r border-n-weak pr-2">
|
||||
{{ formatDate(order.created_at) }}
|
||||
</span>
|
||||
<span class="text-n-slate-11 pl-2">
|
||||
{{ formatCurrency(order.total_price, order.currency) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="fulfillmentStatus">
|
||||
<span
|
||||
:class="getFulfillmentClass(order.fulfillment_status)"
|
||||
class="capitalize font-medium"
|
||||
:title="fulfillmentStatus"
|
||||
>
|
||||
{{ fulfillmentStatus }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ShopifyAPI from '../../../api/integrations/shopify';
|
||||
import ShopifyOrderItem from './ShopifyOrderItem.vue';
|
||||
|
||||
const props = defineProps({
|
||||
contactId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const contact = useFunctionGetter('contacts/getContact', props.contactId);
|
||||
|
||||
const hasSearchableInfo = computed(
|
||||
() => !!contact.value?.email || !!contact.value?.phone_number
|
||||
);
|
||||
|
||||
const orders = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await ShopifyAPI.getOrders(props.contactId);
|
||||
orders.value = response.data.orders;
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e.response?.data?.error || 'CONVERSATION_SIDEBAR.SHOPIFY.ERROR';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contactId,
|
||||
() => {
|
||||
if (hasSearchableInfo.value) {
|
||||
fetchOrders();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-4 py-2 text-n-slate-12">
|
||||
<div v-if="!hasSearchableInfo" class="text-center text-n-slate-12">
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
|
||||
</div>
|
||||
<div v-else-if="loading" class="flex justify-center items-center p-4">
|
||||
<Spinner size="32" class="text-n-brand" />
|
||||
</div>
|
||||
<div v-else-if="error" class="text-center text-n-ruby-12">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else-if="!orders.length" class="text-center text-n-slate-12">
|
||||
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShopifyOrderItem
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
:order="order"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+174
-172
@@ -166,188 +166,190 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal
|
||||
v-model:show="show"
|
||||
full-width
|
||||
:show-close-button="false"
|
||||
:on-close="onClose"
|
||||
>
|
||||
<div
|
||||
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
|
||||
@click="onClose"
|
||||
<Teleport to="body">
|
||||
<woot-modal
|
||||
v-model:show="show"
|
||||
full-width
|
||||
:show-close-button="false"
|
||||
:on-close="onClose"
|
||||
>
|
||||
<header
|
||||
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
|
||||
@click.stop
|
||||
<div
|
||||
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
|
||||
@click="onClose"
|
||||
>
|
||||
<div
|
||||
v-if="senderDetails"
|
||||
class="flex items-center min-w-[15rem] shrink-0"
|
||||
<header
|
||||
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
|
||||
@click.stop
|
||||
>
|
||||
<Thumbnail
|
||||
v-if="senderDetails.avatar"
|
||||
:username="senderDetails.name"
|
||||
:src="senderDetails.avatar"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
|
||||
<h3 class="text-base leading-5 m-0 font-medium">
|
||||
<span
|
||||
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ senderDetails.name }}
|
||||
</span>
|
||||
</h3>
|
||||
<span
|
||||
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
|
||||
>
|
||||
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-2 shrink-0">
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-in"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-out"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(-0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('counter-clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-cw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-lucide-download"
|
||||
slate
|
||||
ghost
|
||||
:is-loading="isDownloading"
|
||||
:disabled="isDownloading"
|
||||
@click="onClickDownload"
|
||||
/>
|
||||
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex items-stretch flex-1 h-full overflow-hidden">
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-left"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
v-if="isImage"
|
||||
:style="imageWrapperStyle"
|
||||
class="flex items-center justify-center origin-center"
|
||||
:class="{
|
||||
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
|
||||
// and prevent image from overflowing container in different aspect ratios
|
||||
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
|
||||
activeImageRotation % 180 !== 0,
|
||||
'size-full': activeImageRotation % 180 === 0,
|
||||
}"
|
||||
v-if="senderDetails"
|
||||
class="flex items-center min-w-[15rem] shrink-0"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
:style="imageStyle"
|
||||
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
|
||||
@click.stop
|
||||
@dblclick.stop="onDoubleClickZoomImage"
|
||||
@wheel.prevent.stop="onWheelImageZoom"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
<Thumbnail
|
||||
v-if="senderDetails.avatar"
|
||||
:username="senderDetails.name"
|
||||
:src="senderDetails.avatar"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
|
||||
<h3 class="text-base leading-5 m-0 font-medium">
|
||||
<span
|
||||
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ senderDetails.name }}
|
||||
</span>
|
||||
</h3>
|
||||
<span
|
||||
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
|
||||
>
|
||||
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-2 shrink-0">
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-in"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-out"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(-0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('counter-clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-cw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-lucide-download"
|
||||
slate
|
||||
ghost
|
||||
:is-loading="isDownloading"
|
||||
:disabled="isDownloading"
|
||||
@click="onClickDownload"
|
||||
/>
|
||||
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex items-stretch flex-1 h-full overflow-hidden">
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-left"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
controls
|
||||
playsInline
|
||||
class="max-h-full max-w-full object-contain"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="flex-1 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
v-if="isImage"
|
||||
:style="imageWrapperStyle"
|
||||
class="flex items-center justify-center origin-center"
|
||||
:class="{
|
||||
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
|
||||
// and prevent image from overflowing container in different aspect ratios
|
||||
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
|
||||
activeImageRotation % 180 !== 0,
|
||||
'size-full': activeImageRotation % 180 === 0,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
:style="imageStyle"
|
||||
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
|
||||
@click.stop
|
||||
@dblclick.stop="onDoubleClickZoomImage"
|
||||
@wheel.prevent.stop="onWheelImageZoom"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="activeAttachment.message_id"
|
||||
controls
|
||||
class="w-full max-w-md"
|
||||
@click.stop
|
||||
>
|
||||
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
controls
|
||||
playsInline
|
||||
class="max-h-full max-w-full object-contain"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="activeAttachment.message_id"
|
||||
controls
|
||||
class="w-full max-w-md"
|
||||
@click.stop
|
||||
>
|
||||
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<footer
|
||||
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
|
||||
>
|
||||
<div
|
||||
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer
|
||||
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
|
||||
>
|
||||
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</woot-modal>
|
||||
<div
|
||||
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
|
||||
>
|
||||
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user