crm/frontend/src/components/Activities.vue
2023-08-31 13:12:37 +05:30

493 lines
16 KiB
Vue

<template>
<div class="p-5 flex items-center justify-between font-medium text-lg">
<div>{{ title }}</div>
<Button v-if="title == 'Calls'" variant="solid" @click="emit('makeCall')">
<PhoneIcon class="w-4 h-4" />
</Button>
<Button
v-else-if="title == 'Notes'"
variant="solid"
@click="emit('makeNote')"
>
<FeatherIcon name="plus" class="w-4 h-4" />
</Button>
</div>
<div v-if="activities.length" class="overflow-y-auto">
<div v-if="title == 'Notes'" class="grid grid-cols-3 gap-4 p-5 pt-0">
<div
v-for="note in activities"
class="group flex flex-col justify-between gap-2 px-4 py-3 border rounded-lg h-48 shadow-sm hover:bg-gray-50 cursor-pointer"
@click="emit('makeNote', note)"
>
<div class="flex items-center justify-between">
<div class="text-lg font-medium truncate">
{{ note.title }}
</div>
<Dropdown
:options="[
{
icon: 'trash-2',
label: 'Delete',
onClick: () => emit('deleteNote', note.name),
},
]"
@click.stop
class="h-6 w-6"
>
<Button
icon="more-horizontal"
variant="ghosted"
class="hover:bg-white !h-6 !w-6"
/>
</Dropdown>
</div>
<TextEditor
v-if="note.content"
:content="note.content"
:editable="false"
editor-class="!prose-sm max-w-none !text-sm text-gray-600 focus:outline-none"
class="flex-1 overflow-hidden"
/>
<div class="flex items-center justify-between mt-1 gap-2">
<div class="flex items-center gap-2">
<UserAvatar :user="note.owner" size="xs" />
<div class="text-sm text-gray-800">
{{ getUser(note.owner).full_name }}
</div>
</div>
<Tooltip :text="dateFormat(note.modified, dateTooltipFormat)">
<div class="text-sm text-gray-700">
{{ timeAgo(note.modified) }}
</div>
</Tooltip>
</div>
</div>
</div>
<div v-else-if="title == 'Calls'">
<div v-for="(call, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-5">
<div
class="relative flex justify-center after:absolute after:border-l after:border-gray-300 after:top-0 after:left-[50%] after:-z-10"
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
>
<div
class="flex items-center justify-center rounded-full outline outline-4 outline-white w-6 h-6 bg-gray-200 mt-[15px] z-10"
>
<FeatherIcon
:name="
call.type == 'Incoming' ? 'phone-incoming' : 'phone-outgoing'
"
class="w-3.5 h-3.5 text-gray-600"
/>
</div>
</div>
<div
class="flex flex-col gap-3 border rounded-lg p-4 mb-3 shadow-sm max-w-[60%]"
>
<div class="flex items-center justify-between">
<div>
{{ call.type == 'Incoming' ? 'Inbound' : 'Outbound' }} call
</div>
<div>
<Tooltip
class="text-gray-600 text-sm"
:text="dateFormat(call.creation, dateTooltipFormat)"
>
{{ timeAgo(call.creation) }}
</Tooltip>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<DurationIcon class="w-4 h-4 text-gray-600" />
<div class="text-sm text-gray-600">Duration</div>
<div class="text-sm">
{{ call.duration }}
</div>
</div>
<div
class="flex items-center gap-1 cursor-pointer select-none"
@click="call.show_recording = !call.show_recording"
>
<PlayIcon class="w-4 h-4 text-gray-600" />
<div class="text-sm text-gray-600">
{{
call.show_recording ? 'Hide recording' : 'Listen to call'
}}
</div>
</div>
</div>
<div
v-if="call.show_recording"
class="flex items-center justify-between border rounded"
>
<audio
class="audio-control"
controls
:src="call.recording_url"
></audio>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<Avatar
:image="call.caller.image"
:label="call.caller.label"
size="xl"
/>
<div class="flex flex-col gap-1 ml-1">
<div class="text-base font-medium">
{{ call.caller.label }}
</div>
<div class="text-xs text-gray-600">
{{ call.from }}
</div>
</div>
<FeatherIcon
name="arrow-right"
class="w-5 h-5 text-gray-600 mx-2"
/>
<Avatar
:image="call.receiver.image"
:label="call.receiver.label"
size="xl"
/>
<div class="flex flex-col gap-1 ml-1">
<div class="text-base font-medium">
{{ call.receiver.label }}
</div>
<div class="text-xs text-gray-600">
{{ call.to }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else v-for="(activity, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-5">
<div
class="relative flex justify-center after:absolute after:border-l after:border-gray-300 after:top-0 after:left-[50%] after:-z-10"
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
>
<div
class="flex items-center justify-center rounded-full outline outline-4 outline-white w-6 h-6 bg-gray-200 z-10"
:class="{ 'mt-[15px]': activity.activity_type == 'communication' }"
>
<FeatherIcon
:name="activity.icon"
class="w-3.5 h-3.5 text-gray-600"
/>
</div>
</div>
<div v-if="activity.activity_type == 'communication'" class="pb-6">
<div
class="shadow-sm border max-w-[80%] rounded-xl p-3 text-base cursor-pointer leading-6 transition-all duration-300 ease-in-out"
>
<div class="flex items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-2">
<UserAvatar :user="activity.data.sender" size="md" />
<span>{{ activity.data.sender_full_name }}</span>
<span>&middot;</span>
<Tooltip
class="text-gray-600 text-sm"
:text="dateFormat(activity.creation, dateTooltipFormat)"
>
{{ timeAgo(activity.creation) }}
</Tooltip>
</div>
<div>
<Button
variant="ghost"
icon="more-horizontal"
class="text-gray-600"
/>
</div>
</div>
<div class="px-1" v-html="activity.data.content" />
</div>
</div>
<div
v-else-if="
activity.activity_type == 'incoming_call' ||
activity.activity_type == 'outgoing_call'
"
class="flex flex-col gap-3 border rounded-lg p-4 mb-3 shadow-sm max-w-[60%]"
>
<div class="flex items-center justify-between">
<div>
{{ activity.type == 'Incoming' ? 'Inbound' : 'Outbound' }} call
</div>
<div>
<Tooltip
class="text-gray-600 text-sm"
:text="dateFormat(activity.creation, dateTooltipFormat)"
>
{{ timeAgo(activity.creation) }}
</Tooltip>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<DurationIcon class="w-4 h-4 text-gray-600" />
<div class="text-sm text-gray-600">Duration</div>
<div class="text-sm">
{{ activity.duration }}
</div>
</div>
<div
class="flex items-center gap-1 cursor-pointer select-none"
@click="activity.show_recording = !activity.show_recording"
>
<PlayIcon class="w-4 h-4 text-gray-600" />
<div class="text-sm text-gray-600">
{{ activity.show_recording ? 'Hide recording' : 'Listen to call' }}
</div>
</div>
</div>
<div
v-if="activity.show_recording"
class="flex items-center justify-between border rounded"
>
<audio
class="audio-control"
controls
:src="activity.recording_url"
></audio>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<Avatar
:image="activity.caller.image"
:label="activity.caller.label"
size="xl"
/>
<div class="flex flex-col gap-1 ml-1">
<div class="text-base font-medium">
{{ activity.caller.label }}
</div>
<div class="text-xs text-gray-600">
{{ activity.from }}
</div>
</div>
<FeatherIcon
name="arrow-right"
class="w-5 h-5 text-gray-600 mx-2"
/>
<Avatar
:image="activity.receiver.image"
:label="activity.receiver.label"
size="xl"
/>
<div class="flex flex-col gap-1 ml-1">
<div class="text-base font-medium">
{{ activity.receiver.label }}
</div>
<div class="text-xs text-gray-600">
{{ activity.to }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="flex flex-col gap-3 pb-6">
<div
class="flex items-start justify-stretch gap-2 text-base leading-6"
>
<UserAvatar :user="activity.owner" size="md" />
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="text-gray-900">{{ activity.owner_name }}</span>
<span v-if="activity.type">{{ activity.type }}</span>
<span
v-if="activity.data.field_label"
class="text-gray-900 truncate max-w-xs"
>
{{ activity.data.field_label }}
</span>
<span v-if="activity.value">{{ activity.value }}</span>
<span
v-if="activity.data.old_value"
class="text-gray-900 truncate max-w-xs"
>
{{ activity.data.old_value }}
</span>
<span v-if="activity.to">to</span>
<span
v-if="activity.data.value"
class="text-gray-900 truncate max-w-xs"
>
{{ activity.data.value }}
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip
:text="dateFormat(activity.creation, dateTooltipFormat)"
class="text-sm text-gray-600 leading-6"
>
{{ timeAgo(activity.creation) }}
</Tooltip>
</div>
</div>
<div
v-if="activity.activity_type == 'comment'"
class="py-3 px-4 rounded-xl shadow-sm border max-w-[80%] text-base cursor-pointer leading-6 transition-all duration-300 ease-in-out"
v-html="activity.data"
/>
</div>
</div>
</div>
</div>
<div
v-else
class="flex-1 flex flex-col gap-3 items-center justify-center font-medium text-xl text-gray-500"
>
<component :is="emptyTextIcon" class="w-10 h-10" />
<span>{{ emptyText }}</span>
<Button
v-if="title == 'Calls'"
variant="solid"
label="Make a call"
@click="emit('makeCall')"
/>
<Button
v-else-if="title == 'Notes'"
variant="solid"
label="Create note"
@click="emit('makeNote')"
/>
<Button
v-else-if="title == 'Emails'"
variant="solid"
label="Send email"
@click="emit('setFocusOnEmail')"
/>
</div>
</template>
<script setup>
import UserAvatar from '@/components/UserAvatar.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import DurationIcon from '@/components/Icons/DurationIcon.vue'
import PlayIcon from '@/components/Icons/PlayIcon.vue'
import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils'
import { usersStore } from '@/stores/users'
import {
Button,
FeatherIcon,
Tooltip,
Dropdown,
TextEditor,
Avatar,
} from 'frappe-ui'
import { computed, h } from 'vue'
const { getUser } = usersStore()
const props = defineProps({
title: {
type: String,
default: 'Activity',
},
activities: {
type: Array,
default: [],
},
})
const emit = defineEmits([
'makeCall',
'makeNote',
'deleteNote',
'setFocusOnEmail',
])
const activities = computed(() => {
if (props.title == 'Calls') {
props.activities.forEach((activity) => {
activity.show_recording = false
})
return props.activities
}
props.activities.forEach((activity) => {
activity.owner_name = getUser(activity.owner).full_name
activity.icon = timelineIcon(activity.activity_type)
activity.type = ''
activity.value = ''
activity.to = ''
if (activity.activity_type == 'creation') {
activity.type = activity.data
} else if (activity.activity_type == 'comment') {
activity.type = 'added a comment'
} else if (activity.activity_type == 'added') {
activity.type = 'added'
activity.value = 'value as'
} else if (activity.activity_type == 'removed') {
activity.type = 'removed'
activity.value = 'value'
} else if (activity.activity_type == 'changed') {
activity.type = 'changed'
activity.value = 'value from'
activity.to = 'to'
}
})
return props.activities
})
const emptyText = computed(() => {
let text = 'No emails communications'
if (props.title == 'Calls') {
text = 'No call logs'
} else if (props.title == 'Notes') {
text = 'No notes'
}
return text
})
const emptyTextIcon = computed(() => {
let icon = EmailIcon
if (props.title == 'Calls') {
icon = PhoneIcon
} else if (props.title == 'Notes') {
icon = NoteIcon
}
return h(icon, { class: 'text-gray-500' })
})
function timelineIcon(activity_type) {
if (activity_type == 'creation') {
return 'plus'
} else if (activity_type == 'removed') {
return 'trash-2'
} else if (activity_type == 'communication') {
return 'at-sign'
} else if (activity_type == 'comment') {
return 'file-text'
} else if (activity_type == 'incoming_call') {
return 'phone-incoming'
} else if (activity_type == 'outgoing_call') {
return 'phone-outgoing'
}
return 'edit'
}
</script>
<style scoped>
.audio-control {
width: 100%;
height: 40px;
outline: none;
border: none;
background: none;
cursor: pointer;
}
.audio-control::-webkit-media-controls-panel {
background-color: white;
}
</style>