first commit

This commit is contained in:
Inshal
2024-05-29 22:34:28 +05:00
commit e63fc41a20
1470 changed files with 174828 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
<script setup>
const assignmentData = [
{
title: 'User Experience Design',
tasks: 120,
progress: 72,
color: 'primary',
},
{
title: 'Basic fundamentals',
tasks: 32,
progress: 48,
color: 'success',
},
{
title: 'React Native components',
tasks: 182,
progress: 15,
color: 'error',
},
{
title: 'Basic of music theory',
tasks: 56,
progress: 24,
color: 'info',
},
]
</script>
<template>
<VCard>
<VCardItem title="Assignment progress">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="assignment in assignmentData"
:key="assignment.title"
>
<template #prepend>
<VProgressCircular
v-model="assignment.progress"
:size="54"
class="me-4"
:color="assignment.color"
>
<h6 class="text-h6">
{{ assignment.progress }}%
</h6>
</VProgressCircular>
</template>
<template #title>
<div class="text-h6 me-4 mb-2 text-truncate">
{{ assignment.title }}
</div>
</template>
<VListItemSubtitle>{{ assignment.tasks }} Tasks</VListItemSubtitle>
<template #append>
<VBtn
variant="tonal"
color="secondary"
class="rounded"
size="34"
>
<VIcon
icon="ri-arrow-right-s-line"
size="20"
class="flip-in-rtl"
/>
</VBtn>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,238 @@
<script setup>
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
// Topics Charts config
const topicsChartConfig = {
chart: {
height: 270,
type: 'bar',
toolbar: { show: false },
},
plotOptions: {
bar: {
horizontal: true,
barHeight: '70%',
distributed: true,
borderRadius: 7,
borderRadiusApplication: 'end',
},
},
colors: [
'rgba(var(--v-theme-primary),1)',
'#16B1FF',
'#56CA00',
'#8A8D93',
'#FF4C51',
'#FFB400',
],
grid: {
borderColor,
strokeDashArray: 10,
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
padding: {
top: -35,
bottom: -12,
},
},
dataLabels: {
enabled: true,
style: {
colors: ['#fff'],
fontWeight: 200,
fontSize: '13px',
},
offsetX: 0,
dropShadow: { enabled: false },
formatter(val, opt) {
return topicsChartConfig.labels[opt.dataPointIndex]
},
},
labels: [
'UI Design',
'UX Design',
'Music',
'Animation',
'Vue',
'SEO',
],
xaxis: {
categories: [
'6',
'5',
'4',
'3',
'2',
'1',
],
axisBorder: { show: false },
axisTicks: { show: false },
labels: {
style: {
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
fontSize: '13px',
},
formatter(val) {
return `${ val }%`
},
},
},
yaxis: {
max: 35,
labels: {
style: {
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
fontSize: '13px',
},
},
},
tooltip: {
enabled: true,
style: { fontSize: '12px' },
onDatasetHover: { highlightDataSeries: false },
},
legend: { show: false },
}
const topicsChartSeries = [{
data: [
35,
20,
14,
12,
10,
9,
],
}]
const topicsData = [
{
title: 'UI Design',
value: 35,
color: 'primary',
},
{
title: 'UX Design',
value: 20,
color: 'info',
},
{
title: 'Music',
value: 14,
color: 'success',
},
]
const moreTopics = [
{
title: 'Animation',
value: 12,
color: 'secondary',
},
{
title: 'Vue',
value: 10,
color: 'error',
},
{
title: 'SEO',
value: 9,
color: 'warning',
},
]
</script>
<template>
<VCard class="topicCard">
<VCardItem title="Topic you are interested in">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<VRow>
<VCol
cols="12"
sm="6"
>
<VueApexCharts
type="bar"
height="300"
:options="topicsChartConfig"
:series="topicsChartSeries"
class="mb-md-0 mb-6"
/>
</VCol>
<VCol class="d-flex justify-space-around align-center">
<div class="d-flex flex-column gap-y-12">
<div
v-for="topic in topicsData"
:key="topic.title"
class="d-flex gap-x-2"
>
<VBadge
inline
dot
:color="topic.color"
class="mt-1"
/>
<div>
<div
class="text-body-1"
style="min-inline-size: 90px;"
>
{{ topic.title }}
</div>
<h5 class="text-h5">
{{ topic.value }}%
</h5>
</div>
</div>
</div>
<div class="d-flex flex-column gap-y-12">
<div
v-for="topic in moreTopics"
:key="topic.title"
class="d-flex gap-x-2"
>
<VBadge
inline
dot
:color="topic.color"
class="mt-1"
/>
<div>
<div
class="text-body-1"
style="min-inline-size: 90px;"
>
{{ topic.title }}
</div>
<h5 class="text-h5">
{{ topic.value }}%
</h5>
</div>
</div>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
@use "@core-scss/template/libs/apex-chart.scss";
.topicCard{
.v-badge.v-badge--dot{
.v-badge__badge{
border-radius: 6px;
block-size: 12px;
inline-size: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,61 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar4 from '@images/avatars/avatar-4.png'
</script>
<template>
<VCard>
<VCardItem title="Popular Instructors">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VDivider />
<div class="text-overline d-flex justify-space-between px-5 py-4">
<div>instructors</div>
<div>Courses</div>
</div>
<VDivider />
<VCardText>
<VList class="card-list">
<VListItem
v-for="instructor in [
{ name: 'Jordan Stevenson', profession: 'Business Intelligence', totalCourses: 33, avatar: avatar1 },
{ name: 'Bentlee Emblin', profession: 'Digital Marketing', totalCourses: 52, avatar: avatar2 },
{ name: 'Benedetto Rossiter', profession: 'UI/UX Design', totalCourses: 12, avatar: avatar3 },
{ name: 'Beverlie Krabbe', profession: 'Vue', totalCourses: 8, avatar: avatar4 },
]"
:key="instructor.name"
>
<template #prepend>
<VAvatar
size="34"
:image="instructor.avatar"
/>
</template>
<h6 class="text-h6">
{{ instructor.name }}
</h6>
<div class="text-caption text-medium-emphasis">
{{ instructor.profession }}
</div>
<template #append>
<div class="text-body-1 text-high-emphasis">
{{ instructor.totalCourses }}
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list{
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,82 @@
<script setup>
const coursesData = [
{
title: 'Videography Basic Design Course',
views: '1.2k',
icon: 'ri-video-download-line',
color: 'primary',
},
{
title: 'Basic Front-end Development Course',
views: '834',
icon: 'ri-code-view',
color: 'info',
},
{
title: 'Basic Fundamentals of Photography',
views: '3.7k',
icon: 'ri-image-2-line',
color: 'success',
},
{
title: 'Advance Dribble Base Visual Design',
views: '2.5k',
icon: 'ri-palette-line',
color: 'warning',
},
{
title: 'Your First Singing Lesson',
views: '948',
icon: 'ri-music-2-line',
color: 'error',
},
]
</script>
<template>
<VCard>
<VCardItem title="Top Courses">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="(course, index) in coursesData"
:key="index"
>
<template #prepend>
<VAvatar
rounded
variant="tonal"
:color="course.color"
>
<VIcon
:icon="course.icon"
size="24"
/>
</VAvatar>
</template>
<template #title>
<div class="text-h6 clamp-text text-wrap me-4">
{{ course.title }}
</div>
</template>
<template #append>
<VChip
variant="tonal"
color="secondary"
size="small"
>
{{ course.views }} Views
</VChip>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,219 @@
<script setup>
const searchQuery = ref('')
// Data table options
const itemsPerPage = ref(5)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const headers = [
{
title: 'Course Name',
key: 'courseName',
},
{
title: 'Time',
key: 'time',
sortable: false,
},
{
title: 'Progress',
key: 'progress',
},
{
title: 'Status',
key: 'status',
sortable: false,
},
]
const { data: courseData } = await useApi(createUrl('/apps/academy/courses', {
query: {
q: searchQuery,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const courses = computed(() => courseData.value.courses)
const totalCourse = computed(() => courseData.value.total)
</script>
<template>
<VCard>
<VCardText>
<div class="d-flex flex-wrap justify-space-between align-center gap-4">
<h5 class="text-h5">
Courses you are taking
</h5>
<VTextField
v-model="searchQuery"
placeholder="Search Course"
density="compact"
style="max-inline-size: 300px; min-inline-size: 200px;"
/>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
:items-per-page-options="[
{ value: 5, title: '5' },
{ value: 10, title: '10' },
{ value: 20, title: '20' },
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
]"
:headers="headers"
:items="courses"
item-value="id"
:items-length="totalCourse"
show-select
class="text-no-wrap"
@update:options="updateOptions"
>
<template #item.courseName="{ item }">
<div class="d-flex align-center gap-x-4 py-2">
<VAvatar
variant="tonal"
rounded
:color="item.color"
>
<VIcon
:icon="item.logo"
size="28"
/>
</VAvatar>
<div>
<RouterLink
:to="{ name: 'apps-academy-course-details' }"
class="d-inline-block text-h6 mb-1"
>
{{ item.courseTitle }}
</RouterLink>
<div class="d-flex align-center">
<VAvatar
size="22"
:image="item.image"
class="me-2"
/>
<div class="text-body-2 text-high-emphasis">
{{ item.user }}
</div>
</div>
</div>
</div>
</template>
<template #item.time="{ item }">
<h6 class="text-h6">
{{ item.time }}
</h6>
</template>
<template #item.progress="{ item }">
<div
class="d-flex align-center gap-x-4 mb-2"
style="inline-size: 15.625rem;"
>
<div class="text-no-wrap text-h6">
{{ Math.floor((item.completedTasks / item.totalTasks) * 100) }}%
</div>
<div class="w-100">
<VProgressLinear
color="primary"
height="8"
:model-value="Math.floor((item.completedTasks / item.totalTasks) * 100)"
rounded
/>
</div>
<div class="text-body-2">
{{ item.completedTasks }}/{{ item.totalTasks }}
</div>
</div>
</template>
<template #item.status="{ item }">
<div class="d-flex justify-space-between gap-x-4">
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="ri-group-line"
color="primary"
size="24"
/>
<span class="text-body-1">
{{ item.userCount }}
</span>
</div>
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="ri-computer-line"
color="info"
size="24"
/>
<span class="text-body-1">{{ item.note }}</span>
</div>
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="ri-video-upload-line"
color="error"
size="24"
/>
<span class="text-body-1">{{ item.view }}</span>
</div>
</div>
</template>
<!-- Pagination -->
<template #bottom>
<VDivider />
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
Rows Per Page:
<VSelect
v-model="itemsPerPage"
class="per-page-select"
variant="plain"
:items="[10, 20, 25, 50, 100]"
/>
</div>
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalCourse) }}
</p>
<div class="d-flex gap-x-2 align-center me-2">
<VBtn
class="flip-in-rtl text-high-emphasis"
icon="ri-arrow-left-s-line"
variant="text"
density="comfortable"
color="high-emphasis"
:disabled="page <= 1"
@click="page <= 1 ? page = 1 : page--"
/>
<VBtn
class="flip-in-rtl text-high-emphasis"
icon="ri-arrow-right-s-line"
density="comfortable"
variant="text"
color="high-emphasis"
:disabled="page >= Math.ceil(totalCourse / itemsPerPage)"
@click="page >= Math.ceil(totalCourse / itemsPerPage) ? page = Math.ceil(totalCourse / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,234 @@
<script setup>
const props = defineProps({
searchQuery: {
type: String,
required: true,
},
})
const itemsPerPage = ref(6)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const hideCompleted = ref(true)
const label = ref('All Courses')
const { data: coursesData } = await useApi(createUrl('/apps/academy/courses', {
query: {
q: () => props.searchQuery,
hideCompleted,
label,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const courses = computed(() => coursesData.value.courses)
const totalCourse = computed(() => coursesData.value.total)
watch([
hideCompleted,
label,
() => props.searchQuery,
], () => {
page.value = 1
})
const resolveChipColor = tags => {
if (tags === 'Web')
return 'primary'
if (tags === 'Art')
return 'success'
if (tags === 'UI/UX')
return 'error'
if (tags === 'Psychology')
return 'warning'
if (tags === 'Design')
return 'info'
}
</script>
<template>
<VCard class="mb-6">
<VCardText>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap gap-4 mb-6">
<div>
<h5 class="text-h5">
My Courses
</h5>
<div class="text-body-1">
Total 6 course you have purchased
</div>
</div>
<div class="d-flex flex-wrap align-center gap-y-4 gap-x-6">
<VSelect
v-model="label"
density="compact"
:items="[
{ title: 'Web', value: 'web' },
{ title: 'Art', value: 'art' },
{ title: 'UI/UX', value: 'ui/ux' },
{ title: 'Psychology', value: 'psychology' },
{ title: 'Design', value: 'design' },
{ title: 'All Courses', value: 'All Courses' },
]"
style="min-inline-size: 250px;"
/>
<VSwitch
v-model="hideCompleted"
label="Hide Completed"
/>
</div>
</div>
<!-- 👉 Course List -->
<div class="mb-6">
<VRow class="match-height">
<template
v-for="course in courses"
:key="course.id"
>
<VCol
cols="12"
md="4"
sm="6"
>
<VCard
flat
border
>
<div class="pa-2">
<VImg
:src="course.tutorImg"
class="cursor-pointer"
@click="() => $router.push({ name: 'apps-academy-course-details' })"
/>
</div>
<VCardText class="pt-3">
<div class="d-flex justify-space-between align-center mb-4">
<VChip
variant="tonal"
:color="resolveChipColor(course.tags)"
size="small"
>
{{ course.tags }}
</VChip>
<div class="d-flex">
<h6 class="text-h6 text-medium-emphasis me-1">
{{ course.rating }}
</h6>
<VIcon
icon="ri-star-fill"
color="warning"
class="me-2"
/>
<div class="text-body-1">
({{ course.ratingCount }})
</div>
</div>
</div>
<h5 class="text-h5 mb-1">
<RouterLink
:to="{ name: 'apps-academy-course-details' }"
class="course-title"
>
{{ course.courseTitle }}
</RouterLink>
</h5>
<p>
{{ course.desc }}
</p>
<div
v-if="course.completedTasks !== course.totalTasks"
class="d-flex align-center mb-1"
>
<VIcon
icon="ri-time-line"
size="20"
class="me-1"
/>
<div class="text-body-1 my-auto">
{{ course.time }}
</div>
</div>
<div
v-else
class="mb-2"
>
<VIcon
icon="ri-check-line"
color="success"
class="me-1"
/>
<span class="text-success text-body-1">Completed</span>
</div>
<VProgressLinear
:model-value="(course.completedTasks / course.totalTasks) * 100"
rounded
rounded-bar
color="primary"
height="8"
class="mb-4"
/>
<div class="d-flex flex-wrap gap-4">
<VBtn
variant="outlined"
color="secondary"
class="flex-grow-1"
:to="{ name: 'apps-academy-course-details' }"
>
<template #prepend>
<VIcon
icon="ri-refresh-line"
class="flip-in-rtl"
/>
</template>
Start Over
</VBtn>
<VBtn
v-if="course.completedTasks !== course.totalTasks"
variant="outlined"
class="flex-grow-1"
:to="{ name: 'apps-academy-course-details' }"
>
<template #append>
<VIcon
icon="ri-arrow-right-line"
class="flip-in-rtl"
/>
</template>
Continue
</VBtn>
</div>
</VCardText>
</VCard>
</VCol>
</template>
</VRow>
</div>
<VPagination
v-model="page"
rounded
color="primary"
:length="Math.ceil(totalCourse / itemsPerPage)"
/>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.course-title{
&:not(:hover){
color: rgba(var(--v-theme-on-surface), var(--v-text-high-emphasis))
}
}
</style>

View File

@@ -0,0 +1,51 @@
<script setup>
import girlWithLaptop from '@images/pages/pose-fs-9.png'
</script>
<template>
<VCard>
<VCardText>
<div class="d-flex justify-center align-start pb-0 px-3 pt-3 mb-6 bg-light-primary rounded">
<VImg
:src="girlWithLaptop"
width="145"
height="140"
/>
</div>
<div>
<h5 class="text-h5 mb-1">
Upcoming Webinar
</h5>
<div class="text-body-1">
Next Generation Frontend Architecture Using Layout Engine And Vue.
</div>
<div class="d-flex justify-space-between my-6 gap-4 flex-wrap">
<div
v-for="{ icon, title, value } in [{ icon: 'ri-calendar-line', title: '17 Nov 23', value: 'Date' }, { icon: 'ri-time-line', title: '32 Minutes', value: 'Duration' }]"
:key="title"
class="d-flex gap-x-4 align-center"
>
<VAvatar
color="primary"
variant="tonal"
rounded
>
<VIcon :icon="icon" />
</VAvatar>
<div>
<div class="text-body-1 text-high-emphasis">
{{ title }}
</div>
<div class="text-caption text-medium-emphasis">
{{ value }}
</div>
</div>
</div>
</div>
<VBtn block>
Join the event
</VBtn>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,327 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { VForm } from 'vuetify/components/VForm'
import { useCalendarStore } from './useCalendarStore'
import avatar1 from '@images/avatars/avatar-1.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar5 from '@images/avatars/avatar-5.png'
import avatar6 from '@images/avatars/avatar-6.png'
import avatar7 from '@images/avatars/avatar-7.png'
// 👉 store
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
event: {
type: null,
required: true,
},
})
const emit = defineEmits([
'update:isDrawerOpen',
'addEvent',
'updateEvent',
'removeEvent',
])
const store = useCalendarStore()
const refForm = ref()
// 👉 Event
const event = ref(JSON.parse(JSON.stringify(props.event)))
const resetEvent = () => {
event.value = JSON.parse(JSON.stringify(props.event))
nextTick(() => {
refForm.value?.resetValidation()
})
}
watch(() => props.isDrawerOpen, resetEvent)
const removeEvent = () => {
emit('removeEvent', String(event.value.id))
// Close drawer
emit('update:isDrawerOpen', false)
}
const handleSubmit = () => {
refForm.value?.validate().then(({ valid }) => {
if (valid) {
// If id exist on id => Update event
if ('id' in event.value)
emit('updateEvent', event.value)
// Else => add new event
else
emit('addEvent', event.value)
// Close drawer
emit('update:isDrawerOpen', false)
}
})
}
const guestsOptions = [
{
avatar: avatar1,
name: 'Jane Foster',
},
{
avatar: avatar3,
name: 'Donna Frank',
},
{
avatar: avatar5,
name: 'Gabrielle Robertson',
},
{
avatar: avatar7,
name: 'Lori Spears',
},
{
avatar: avatar6,
name: 'Sandy Vega',
},
{
avatar: avatar2,
name: 'Cheryl May',
},
]
// 👉 Form
const onCancel = () => {
// Close drawer
emit('update:isDrawerOpen', false)
nextTick(() => {
refForm.value?.reset()
resetEvent()
refForm.value?.resetValidation()
})
}
const startDateTimePickerConfig = computed(() => {
const config = {
enableTime: !event.value.allDay,
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
}
if (event.value.end)
config.maxDate = event.value.end
return config
})
const endDateTimePickerConfig = computed(() => {
const config = {
enableTime: !event.value.allDay,
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
}
if (event.value.start)
config.minDate = event.value.start
return config
})
const dialogModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
</script>
<template>
<VNavigationDrawer
temporary
location="end"
:model-value="props.isDrawerOpen"
width="420"
class="scrollable-content"
@update:model-value="dialogModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
:title="event.id ? 'Update Event' : 'Add Event'"
@cancel="$emit('update:isDrawerOpen', false)"
>
<template #beforeClose>
<IconBtn
v-show="event.id"
@click="removeEvent"
>
<VIcon
size="18"
icon="ri-delete-bin-7-line"
/>
</IconBtn>
</template>
</AppDrawerHeaderSection>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<!-- SECTION Form -->
<VForm
ref="refForm"
@submit.prevent="handleSubmit"
>
<VRow>
<!-- 👉 Title -->
<VCol cols="12">
<VTextField
v-model="event.title"
label="Title"
placeholder="Meeting with Jane"
:rules="[requiredValidator]"
/>
</VCol>
<!-- 👉 Calendar -->
<VCol cols="12">
<VSelect
v-model="event.extendedProps.calendar"
label="Label"
placeholder="Select Event Label"
:rules="[requiredValidator]"
:items="store.availableCalendars"
:item-title="item => item.label"
:item-value="item => item.label"
>
<template #selection="{ item }">
<div
v-show="event.extendedProps.calendar"
class="align-center"
:class="event.extendedProps.calendar ? 'd-flex' : ''"
>
<VIcon
size="8"
icon="ri-circle-fill"
:color="item.raw.color"
class="me-2"
/>
<span>{{ item.raw.label }}</span>
</div>
</template>
<template #item="{ item, props: itemProps }">
<VListItem v-bind="itemProps">
<template #prepend>
<VIcon
size="8"
icon="ri-circle-fill"
:color="item.raw.color"
/>
</template>
</VListItem>
</template>
</VSelect>
</VCol>
<!-- 👉 Start date -->
<VCol cols="12">
<AppDateTimePicker
:key="JSON.stringify(startDateTimePickerConfig)"
v-model="event.start"
:rules="[requiredValidator]"
label="Start date"
placeholder="Select Date"
:config="startDateTimePickerConfig"
/>
</VCol>
<!-- 👉 End date -->
<VCol cols="12">
<AppDateTimePicker
:key="JSON.stringify(endDateTimePickerConfig)"
v-model="event.end"
:rules="[requiredValidator]"
label="End date"
placeholder="Select End Date"
:config="endDateTimePickerConfig"
/>
</VCol>
<!-- 👉 All day -->
<VCol cols="12">
<VSwitch
v-model="event.allDay"
label="All day"
/>
</VCol>
<!-- 👉 Event URL -->
<VCol cols="12">
<VTextField
v-model="event.url"
label="Event URL"
placeholder="https://event.com/meeting"
:rules="[urlValidator]"
type="url"
/>
</VCol>
<!-- 👉 Guests -->
<VCol cols="12">
<VSelect
v-model="event.extendedProps.guests"
label="Guests"
placeholder="Select guests"
:items="guestsOptions"
:item-title="item => item.name"
:item-value="item => item.name"
chips
multiple
eager
/>
</VCol>
<!-- 👉 Location -->
<VCol cols="12">
<VTextField
v-model="event.extendedProps.location"
label="Location"
placeholder="Meeting room"
/>
</VCol>
<!-- 👉 Description -->
<VCol cols="12">
<VTextarea
v-model="event.extendedProps.description"
label="Description"
placeholder="Meeting description"
/>
</VCol>
<!-- 👉 Form buttons -->
<VCol cols="12">
<VBtn
type="submit"
class="me-3"
>
Submit
</VBtn>
<VBtn
variant="outlined"
color="secondary"
@click="onCancel"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
<!-- !SECTION -->
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,295 @@
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
import listPlugin from '@fullcalendar/list'
import timeGridPlugin from '@fullcalendar/timegrid'
import { useConfigStore } from '@core/stores/config'
import { useCalendarStore } from '@/views/apps/calendar/useCalendarStore'
export const blankEvent = {
title: '',
start: '',
end: '',
allDay: false,
url: '',
extendedProps: {
/*
We have to use undefined here because if we have blank string as value then select placeholder will be active (moved to top).
Hence, we need to set it to undefined or null
*/
calendar: undefined,
guests: [],
location: '',
description: '',
},
}
export const useCalendar = (event, isEventHandlerSidebarActive, isLeftSidebarOpen) => {
const configStore = useConfigStore()
// 👉 Store
const store = useCalendarStore()
// 👉 Calendar template ref
const refCalendar = ref()
// 👉 Calendar colors
const calendarsColor = {
Business: 'primary',
Holiday: 'success',
Personal: 'error',
Family: 'warning',
ETC: 'info',
}
// Extract event data from event API
const extractEventDataFromEventApi = eventApi => {
const { id, title, start, end, url, extendedProps: { calendar, guests, location, description }, allDay } = eventApi
return {
id,
title,
start,
end,
url,
extendedProps: {
calendar,
guests,
location,
description,
},
allDay,
}
}
if (typeof process !== 'undefined' && process.server)
store.fetchEvents()
// 👉 Fetch events
const fetchEvents = (info, successCallback) => {
// If there's no info => Don't make useless API call
if (!info)
return
store.fetchEvents()
.then(r => {
successCallback(r.map(e => ({
...e,
// Convert string representation of date to Date object
start: new Date(e.start),
end: new Date(e.end),
})))
})
.catch(e => {
console.error('Error occurred while fetching calendar events', e)
})
}
// 👉 Calendar API
const calendarApi = ref(null)
// 👉 Update event in calendar [UI]
const updateEventInCalendar = (updatedEventData, propsToUpdate, extendedPropsToUpdate) => {
const existingEvent = calendarApi.value?.getEventById(String(updatedEventData.id))
if (!existingEvent) {
console.warn('Can\'t found event in calendar to update')
return
}
// ---Set event properties except date related
// Docs: https://fullcalendar.io/docs/Event-setProp
// dateRelatedProps => ['start', 'end', 'allDay']
for (let index = 0; index < propsToUpdate.length; index++) {
const propName = propsToUpdate[index]
existingEvent.setProp(propName, updatedEventData[propName])
}
// --- Set date related props
// ? Docs: https://fullcalendar.io/docs/Event-setDates
existingEvent.setDates(updatedEventData.start, updatedEventData.end, { allDay: updatedEventData.allDay })
// --- Set event's extendedProps
// ? Docs: https://fullcalendar.io/docs/Event-setExtendedProp
for (let index = 0; index < extendedPropsToUpdate.length; index++) {
const propName = extendedPropsToUpdate[index]
existingEvent.setExtendedProp(propName, updatedEventData.extendedProps[propName])
}
}
// 👉 Remove event in calendar [UI]
const removeEventInCalendar = eventId => {
const _event = calendarApi.value?.getEventById(eventId)
if (_event)
_event.remove()
}
// 👉 refetch events
const refetchEvents = () => {
calendarApi.value?.refetchEvents()
}
watch(() => store.selectedCalendars, refetchEvents)
// 👉 Add event
const addEvent = _event => {
store.addEvent(_event)
.then(() => {
refetchEvents()
})
}
// 👉 Update event
const updateEvent = _event => {
store.updateEvent(_event)
.then(r => {
const propsToUpdate = ['id', 'title', 'url']
const extendedPropsToUpdate = ['calendar', 'guests', 'location', 'description']
updateEventInCalendar(r, propsToUpdate, extendedPropsToUpdate)
})
refetchEvents()
}
// 👉 Remove event
const removeEvent = eventId => {
store.removeEvent(eventId).then(() => {
removeEventInCalendar(eventId)
})
}
// 👉 Calendar options
const calendarOptions = {
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
initialView: 'dayGridMonth',
headerToolbar: {
start: 'drawerToggler,prev,next title',
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth',
},
events: fetchEvents,
// ❗ We need this to be true because when its false and event is allDay event and end date is same as start data then Full calendar will set end to null
forceEventDuration: true,
/*
Enable dragging and resizing event
Docs: https://fullcalendar.io/docs/editable
*/
editable: true,
/*
Enable resizing event from start
Docs: https://fullcalendar.io/docs/eventResizableFromStart
*/
eventResizableFromStart: true,
/*
Automatically scroll the scroll-containers during event drag-and-drop and date selecting
Docs: https://fullcalendar.io/docs/dragScroll
*/
dragScroll: true,
/*
Max number of events within a given day
Docs: https://fullcalendar.io/docs/dayMaxEvents
*/
dayMaxEvents: 2,
/*
Determines if day names and week names are clickable
Docs: https://fullcalendar.io/docs/navLinks
*/
navLinks: true,
eventClassNames({ event: calendarEvent }) {
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
return [
// Background Color
`bg-light-${colorName} text-${colorName}`,
]
},
eventClick({ event: clickedEvent, jsEvent }) {
// Prevent the default action
jsEvent.preventDefault()
if (clickedEvent.url) {
// Open the URL in a new tab
window.open(clickedEvent.url, '_blank')
}
// * Only grab required field otherwise it goes in infinity loop
// ! Always grab all fields rendered by form (even if it get `undefined`) otherwise due to Vue3/Composition API you might get: "object is not extensible"
event.value = extractEventDataFromEventApi(clickedEvent)
isEventHandlerSidebarActive.value = true
},
// customButtons
dateClick(info) {
event.value = { ...event.value, start: info.date }
isEventHandlerSidebarActive.value = true
},
/*
Handle event drop (Also include dragged event)
Docs: https://fullcalendar.io/docs/eventDrop
We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
*/
eventDrop({ event: droppedEvent }) {
updateEvent(extractEventDataFromEventApi(droppedEvent))
},
/*
Handle event resize
Docs: https://fullcalendar.io/docs/eventResize
*/
eventResize({ event: resizedEvent }) {
if (resizedEvent.start && resizedEvent.end)
updateEvent(extractEventDataFromEventApi(resizedEvent))
},
customButtons: {
drawerToggler: {
text: 'calendarDrawerToggler',
click() {
isLeftSidebarOpen.value = true
},
},
},
}
// 👉 onMounted
onMounted(() => {
calendarApi.value = refCalendar.value.getApi()
})
// 👉 Jump to date on sidebar(inline) calendar change
const jumpToDate = currentDate => {
calendarApi.value?.gotoDate(new Date(currentDate))
}
watch(() => configStore.isAppRTL, val => {
calendarApi.value?.setOption('direction', val ? 'rtl' : 'ltr')
}, { immediate: true })
return {
refCalendar,
calendarOptions,
refetchEvents,
fetchEvents,
addEvent,
updateEvent,
removeEvent,
jumpToDate,
}
}

View File

@@ -0,0 +1,59 @@
export const useCalendarStore = defineStore('calendar', {
// arrow function recommended for full type inference
state: () => ({
availableCalendars: [
{
color: 'error',
label: 'Personal',
},
{
color: 'primary',
label: 'Business',
},
{
color: 'warning',
label: 'Family',
},
{
color: 'success',
label: 'Holiday',
},
{
color: 'info',
label: 'ETC',
},
],
selectedCalendars: ['Personal', 'Business', 'Family', 'Holiday', 'ETC'],
}),
actions: {
async fetchEvents() {
const { data, error } = await useApi(createUrl('/apps/calendar', {
query: {
calendars: this.selectedCalendars,
},
}))
if (error.value)
return error.value
return data.value
},
async addEvent(event) {
await $api('/apps/calendar', {
method: 'POST',
body: event,
})
},
async updateEvent(event) {
return await $api(`/apps/calendar/${event.id}`, {
method: 'PUT',
body: event,
})
},
async removeEvent(eventId) {
return await $api(`/apps/calendar/${eventId}`, {
method: 'DELETE',
})
},
},
})

View File

@@ -0,0 +1,191 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const emit = defineEmits(['close'])
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
</script>
<template>
<template v-if="store.activeChat">
<!-- Close Button -->
<div
class="pt-2 me-2"
:class="$vuetify.locale.isRtl ? 'text-left' : 'text-right'"
>
<IconBtn @click="$emit('close')">
<VIcon
icon="ri-close-line"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge
location="bottom right"
offset-x="7"
offset-y="4"
bordered
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
class="chat-user-profile-badge mb-4"
>
<VAvatar
size="84"
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
>
<VImg
v-if="store.activeChat.contact.avatar"
:src="store.activeChat.contact.avatar"
/>
<span
v-else
class="text-3xl"
>{{ avatarText(store.activeChat.contact.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.activeChat.contact.fullName }}
</h5>
<p class="text-body-1 mb-0">
{{ store.activeChat.contact.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content text-medium-emphasis pb-5 px-5"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6">
<p
for="textarea-user-about"
class="text-base text-disabled mb-1"
>
ABOUT
</p>
<p class="mb-0">
{{ store.activeChat.contact.about }}
</p>
</div>
<!-- Personal Information -->
<div class="mb-6">
<p class="text-base text-disabled mb-1">
PERSONAL INFORMATION
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-mail-line"
/>
<h6 class="text-h6 font-weight-regular">
lucifer@email.com
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-phone-line"
/>
<h6 class="text-h6 font-weight-regular">
+1(123) 456 - 7890
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
icon="ri-time-line"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Mon - Fri 10AM - 8PM
</h6>
</div>
</div>
<!-- Options -->
<div>
<p class="text-base text-disabled mb-1">
OPTIONS
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-bookmark-line"
/>
<h6 class="text-h6 font-weight-regular">
Add Tag
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-star-line"
/>
<h6 class="text-h6 font-weight-regular">
Important Contact
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
size="22"
color="high-emphasis"
icon="ri-file-image-line"
/>
<h6 class="text-h6 font-weight-regular">
Shared Media
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
icon="ri-delete-bin-line"
size="22"
color="high-emphasis"
class="me-2"
/>
<h6 class="text-h6 font-weight-regular">
Delete Contact
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
size="22"
color="high-emphasis"
icon="ri-forbid-line"
class="me-2"
/>
<h6 class="text-h6 font-weight-regular">
Block Contact
</h6>
</div>
</div>
<VBtn
block
color="error"
append-icon="ri-delete-bin-7-line"
class="mt-12"
>
Delete Contact
</VBtn>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,110 @@
<script setup>
import { useChat } from '@/views/apps/chat/useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const props = defineProps({
isChatContact: {
type: Boolean,
required: false,
default: false,
},
user: {
type: null,
required: true,
},
})
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const isChatContactActive = computed(() => {
const isActive = store.activeChat?.contact.id === props.user.id
if (!props.isChatContact)
return !store.activeChat?.chat && isActive
return isActive
})
</script>
<template>
<li
:key="store.chatsContacts.length"
class="chat-contact cursor-pointer d-flex align-center"
:class="{ 'chat-contact-active': isChatContactActive }"
:data-x="store.chatsContacts.length"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(props.user.status)"
bordered
:model-value="props.isChatContact"
>
<VAvatar
size="40"
:variant="!props.user.avatar ? 'tonal' : undefined"
:color="!props.user.avatar ? resolveAvatarBadgeVariant(props.user.status) : undefined"
>
<VImg
v-if="props.user.avatar"
:src="props.user.avatar"
alt="John Doe"
/>
<span v-else>{{ avatarText(user.fullName) }}</span>
</VAvatar>
</VBadge>
<div class="flex-grow-1 ms-4 overflow-hidden">
<p class="text-base mb-0">
{{ props.user.fullName }}
</p>
<span class="d-block text-body-2 text-truncate">{{ props.isChatContact && 'chat' in props.user ? props.user.chat.lastMessage.message : props.user.about }}</span>
</div>
<div
v-if="props.isChatContact && 'chat' in props.user"
class="d-flex flex-column align-self-start"
>
<span class="d-block text-sm text-disabled whitespace-no-wrap">{{ formatDateToMonthShort(props.user.chat.lastMessage.time) }}</span>
<VBadge
v-if="props.user.chat.unseenMsgs"
color="error"
inline
:content="props.user.chat.unseenMsgs"
class="ms-auto"
/>
</div>
</li>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
@use "@core-scss/base/mixins";
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.chat-contact {
border-radius: vuetify.$border-radius-root;
padding-block: 8px;
padding-inline: var(--chat-content-spacing-x);
@include mixins.before-pseudo;
@include vuetifyStates.states($active: false);
&.chat-contact-active {
@include mixins.elevation(2);
background: rgb(var(--v-theme-primary));
color: #fff;
--v-theme-on-background: #fff;
.v-avatar {
background: #fff;
}
}
.v-badge--bordered .v-badge__badge::after {
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import ChatContact from '@/views/apps/chat/ChatContact.vue'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const props = defineProps({
search: {
type: String,
required: true,
},
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'openChatOfContact',
'showUserProfile',
'close',
'update:search',
])
const { resolveAvatarBadgeVariant } = useChat()
const search = useVModel(props, 'search', emit)
const store = useChatStore()
</script>
<template>
<!-- 👉 Chat list header -->
<div
v-if="store.profileUser"
class="chat-list-header gap-4"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
bordered
>
<VAvatar
class="cursor-pointer"
@click="$emit('showUserProfile')"
>
<VImg
:src="store.profileUser.avatar"
alt="John Doe"
/>
</VAvatar>
</VBadge>
<VTextField
v-model="search"
placeholder="Search..."
prepend-inner-icon="ri-search-line"
density="compact"
class="chat-list-search"
/>
<IconBtn
v-if="$vuetify.display.smAndDown"
@click="$emit('close')"
>
<VIcon
icon="ri-close-line"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<VDivider />
<PerfectScrollbar
tag="ul"
class="chat-contacts-list px-3 d-flex flex-column gap-1"
:options="{ wheelPropagation: false }"
>
<li class="list-none">
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Chats</span>
</li>
<ChatContact
v-for="contact in store.chatsContacts"
:key="`chat-${contact.id}`"
:user="contact"
is-chat-contact
@click="$emit('openChatOfContact', contact.id)"
/>
<span
v-show="!store.chatsContacts.length"
class="no-chat-items-text text-disabled"
>No chats found</span>
<li class="list-none">
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Contacts</span>
</li>
<ChatContact
v-for="contact in store.contacts"
:key="`chat-${contact.id}`"
:user="contact"
@click="$emit('openChatOfContact', contact.id)"
/>
<span
v-show="!store.contacts.length"
class="no-chat-items-text text-disabled"
>No contacts found</span>
</PerfectScrollbar>
</template>
<style lang="scss">
.chat-contacts-list {
--chat-content-spacing-x: 12px;
padding-block-end: 0.75rem;
.chat-contact-header {
margin-block: 1rem 4px;
margin-inline: 1rem;
}
.no-chat-items-text {
margin-inline: var(--chat-content-spacing-x);
}
}
</style>

View File

@@ -0,0 +1,149 @@
<script setup>
import { useChatStore } from '@/views/apps/chat/useChatStore'
const store = useChatStore()
const contact = computed(() => ({
id: store.activeChat?.contact.id,
avatar: store.activeChat?.contact.avatar,
}))
const resolveFeedbackIcon = feedback => {
if (feedback.isSeen)
return {
icon: 'ri-check-double-line',
color: 'success',
}
else if (feedback.isDelivered)
return {
icon: 'ri-check-double-line',
color: undefined,
}
else
return {
icon: 'ri-check-line',
color: undefined,
}
}
const msgGroups = computed(() => {
let messages = []
const _msgGroups = []
if (store.activeChat.chat) {
messages = store.activeChat.chat.messages
let msgSenderId = messages[0].senderId
let msgGroup = {
senderId: msgSenderId,
messages: [],
}
messages.forEach((msg, index) => {
if (msgSenderId === msg.senderId) {
msgGroup.messages.push({
message: msg.message,
time: msg.time,
feedback: msg.feedback,
})
} else {
msgSenderId = msg.senderId
_msgGroups.push(msgGroup)
msgGroup = {
senderId: msg.senderId,
messages: [{
message: msg.message,
time: msg.time,
feedback: msg.feedback,
}],
}
}
if (index === messages.length - 1)
_msgGroups.push(msgGroup)
})
}
return _msgGroups
})
</script>
<template>
<div class="chat-log pa-5">
<div
v-for="(msgGrp, index) in msgGroups"
:key="msgGrp.senderId + String(index)"
class="chat-group d-flex align-start"
:class="[{
'flex-row-reverse': msgGrp.senderId !== contact.id,
'mb-8': msgGroups.length - 1 !== index,
}]"
>
<div
class="chat-avatar"
:class="msgGrp.senderId !== contact.id ? 'ms-4' : 'me-4'"
>
<VAvatar size="32">
<VImg :src="msgGrp.senderId === contact.id ? contact.avatar : store.profileUser?.avatar" />
</VAvatar>
</div>
<div
class="chat-body d-inline-flex flex-column"
:class="msgGrp.senderId !== contact.id ? 'align-end' : 'align-start'"
>
<div
v-for="(msgData, msgIndex) in msgGrp.messages"
:key="msgData.time"
class="chat-content text-body-1 py-2 px-4 elevation-2"
:class="[
msgGrp.senderId === contact.id ? 'bg-surface chat-left' : 'bg-primary text-white chat-right',
msgGrp.messages.length - 1 !== msgIndex ? 'mb-2' : 'mb-1',
]"
>
<p class="mb-0">
{{ msgData.message }}
</p>
</div>
<div
:class="{ 'text-right': msgGrp.senderId !== contact.id }"
class="d-flex align-center gap-2"
>
<VIcon
v-if="msgGrp.senderId !== contact.id"
size="16"
:color="resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).color"
>
{{ resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).icon }}
</VIcon>
<p
class="text-sm text-disabled mb-0"
style="letter-spacing: 0.4px;"
>
{{ formatDate(msgGrp.messages[msgGrp.messages.length - 1].time, { hour: 'numeric', minute: 'numeric' }) }}
</p>
</div>
</div>
</div>
</div>
</template>
<style lang=scss>
.chat-log {
.chat-content {
border-end-end-radius: 6px;
border-end-start-radius: 6px;
p {
overflow-wrap: anywhere;
}
&.bg-surface{
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
}
&.chat-left {
border-start-end-radius: 6px;
}
&.chat-right {
border-start-start-radius: 6px;
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
const emit = defineEmits(['close'])
// composables
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const userStatusRadioOptions = [
{
title: 'Online',
value: 'online',
color: 'success',
},
{
title: 'Away',
value: 'away',
color: 'warning',
},
{
title: 'Do not disturb',
value: 'busy',
color: 'error',
},
{
title: 'Offline',
value: 'offline',
color: 'secondary',
},
]
const isTwoStepVerified = ref(true)
const isNotificationEnabled = ref(false)
</script>
<template>
<template v-if="store.profileUser">
<!-- Close Button -->
<div class="pt-2 me-2 text-end">
<IconBtn @click="$emit('close')">
<VIcon
class="text-medium-emphasis"
icon="ri-close-line"
/>
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge
location="bottom right"
offset-x="7"
offset-y="4"
bordered
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
class="chat-user-profile-badge mb-4"
>
<VAvatar
size="84"
:variant="!store.profileUser.avatar ? 'tonal' : undefined"
:color="!store.profileUser.avatar ? resolveAvatarBadgeVariant(store.profileUser.status) : undefined"
>
<VImg
v-if="store.profileUser.avatar"
:src="store.profileUser.avatar"
/>
<span
v-else
class="text-3xl"
>{{ avatarText(store.profileUser.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.profileUser.fullName }}
</h5>
<p class="text-body-1 text-capitalize mb-0">
{{ store.profileUser.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content pb-5 px-5"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6 text-medium-emphasis">
<p
for="textarea-user-about"
class="text-base text-disabled mb-0"
>
ABOUT
</p>
<VTextarea
id="textarea-user-about"
v-model="store.profileUser.about"
auto-grow
class="mt-1"
rows="3"
/>
</div>
<!-- Status -->
<div class="mb-6">
<p class="text-base text-disabled mb-0">
STATUS
</p>
<VRadioGroup
v-model="store.profileUser.status"
class="ms-2 mt-1"
>
<VRadio
v-for="radioOption in userStatusRadioOptions"
:key="radioOption.title"
:label="radioOption.title"
:value="radioOption.value"
:color="radioOption.color"
/>
</VRadioGroup>
</div>
<!-- Settings -->
<div class="text-medium-emphasis">
<p class="text-base text-disabled mb-0">
SETTINGS
</p>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-lock-password-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Two-step Verification
</h6>
<VSpacer />
<VSwitch v-model="isTwoStepVerified" />
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-notification-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Notification
</h6>
<VSpacer />
<VSwitch v-model="isNotificationEnabled" />
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-user-add-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Invite Friends
</h6>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2"
icon="ri-delete-bin-7-line"
size="22"
color="high-emphasis"
/>
<h6 class="text-h6 font-weight-regular">
Delete Account
</h6>
</div>
</div>
<!-- Logout Button -->
<VBtn
block
color="primary"
class="mt-11"
append-icon="ri-logout-box-r-line"
>
Logout
</VBtn>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,16 @@
export const useChat = () => {
const resolveAvatarBadgeVariant = status => {
if (status === 'online')
return 'success'
if (status === 'busy')
return 'error'
if (status === 'away')
return 'warning'
return 'secondary'
}
return {
resolveAvatarBadgeVariant,
}
}

View File

@@ -0,0 +1,80 @@
export const useChatStore = defineStore('chat', {
// arrow function recommended for full type inference
state: () => ({
contacts: [],
chatsContacts: [],
profileUser: undefined,
activeChat: null,
}),
actions: {
async fetchChatsAndContacts(q) {
const { data, error } = await useApi(createUrl('/apps/chat/chats-and-contacts', {
query: {
q,
},
}))
if (error.value) {
console.log(error.value)
}
else {
const { chatsContacts, contacts, profileUser } = data.value
this.chatsContacts = chatsContacts
this.contacts = contacts
this.profileUser = profileUser
}
},
async getChat(userId) {
const res = await $api(`/apps/chat/chats/${userId}`)
this.activeChat = res
},
async sendMsg(message) {
const senderId = this.profileUser?.id
const response = await $api(`apps/chat/chats/${this.activeChat?.contact.id}`, {
method: 'POST',
body: { message, senderId },
})
const { msg, chat } = response
// ? If it's not undefined => New chat is created (Contact is not in list of chats)
if (chat !== undefined) {
const activeChat = this.activeChat
this.chatsContacts.push({
...activeChat.contact,
chat: {
id: chat.id,
lastMessage: [],
unseenMsgs: 0,
messages: [msg],
},
})
if (this.activeChat) {
this.activeChat.chat = {
id: chat.id,
messages: [msg],
unseenMsgs: 0,
userId: this.activeChat?.contact.id,
}
}
}
else {
this.activeChat?.chat?.messages.push(msg)
}
// Set Last Message for active contact
const contact = this.chatsContacts.find(c => {
if (this.activeChat)
return c.id === this.activeChat.contact.id
return false
})
contact.chat.lastMessage = msg
},
},
})

View File

@@ -0,0 +1,200 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { VForm } from 'vuetify/components/VForm'
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDrawerOpen'])
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
const refVForm = ref()
const name = ref()
const email = ref()
const mobile = ref()
const addressLine1 = ref()
const addressLine2 = ref()
const town = ref()
const state = ref()
const postCode = ref()
const country = ref()
const isBillingAddress = ref(false)
const resetForm = () => {
refVForm.value?.reset()
emit('update:isDrawerOpen', false)
}
</script>
<template>
<VNavigationDrawer
:model-value="props.isDrawerOpen"
temporary
location="end"
width="370"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Add a Customer"
@cancel="$emit('update:isDrawerOpen', false)"
/>
<VDivider />
<VCard flat>
<PerfectScrollbar
:options="{ wheelPropagation: false }"
class="h-100"
>
<VCardText style="block-size: calc(100vh - 5rem);">
<VForm
ref="refVForm"
@submit.prevent=""
>
<VRow>
<VCol>
<div class="text-body-1 font-weight-medium text-high-emphasis">
Basic Information
</div>
</VCol>
<VCol cols="12">
<VTextField
v-model="name"
label="Full Name"
:rules="[requiredValidator]"
placeholder="John Doe"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="email"
label="Email Address"
:rules="[requiredValidator, emailValidator]"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="mobile"
label="Mobile Number"
type="number"
:rules="[requiredValidator]"
placeholder="+(123) 456-7890"
/>
</VCol>
<VCol>
<div class="text-body-1 font-weight-medium text-high-emphasis">
Shipping Information
</div>
</VCol>
<VCol cols="12">
<VTextField
v-model="addressLine1"
label="Address Line 1*"
:rules="[requiredValidator]"
placeholder="45, Rocker Terrace"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="addressLine2"
placeholder="Empire Heights,"
:rules="[requiredValidator]"
label="Address Line 2*"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="town"
label="Town*"
:rules="[requiredValidator]"
placeholder="New York"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="state"
placeholder="Texas"
:rules="[requiredValidator]"
label="State/Province*"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="postCode"
label="Post Code*"
type="number"
:rules="[requiredValidator]"
placeholder="982347"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="country"
placeholder="United States"
:rules="[requiredValidator]"
label="Country"
:items="['United States', 'United Kingdom', 'Canada']"
/>
</VCol>
<VCol cols="12">
<div class="d-flex justify-space-between">
<div class="d-flex flex-column gap-y-1">
<h6 class="text-h6">
Use as a billing address?
</h6>
<span class="text-sm">Please check budget for more info</span>
</div>
<VSwitch v-model="isBillingAddress" />
</div>
</VCol>
<VCol cols="12">
<div class="d-flex justify-start">
<VBtn
type="submit"
color="primary"
class="me-4"
>
Add
</VBtn>
<VBtn
color="error"
variant="outlined"
@click="resetForm"
>
Discard
</VBtn>
</div>
</VCol>
</VRow>
</VForm>
</VCardText>
</PerfectScrollbar>
</VCard>
</VNavigationDrawer>
</template>
<style lang="scss">
.v-navigation-drawer__content {
overflow-y: hidden !important;
}
</style>

View File

@@ -0,0 +1,282 @@
<script setup>
import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Underline } from '@tiptap/extension-underline'
import { StarterKit } from '@tiptap/starter-kit'
import {
EditorContent,
useEditor,
} from '@tiptap/vue-3'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { VForm } from 'vuetify/components/VForm'
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDrawerOpen'])
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
const editor = useEditor({
content: '',
extensions: [
StarterKit,
Image,
Placeholder.configure({ placeholder: 'Write a Comment...' }),
Underline,
Link.configure({ openOnClick: false }),
],
})
const setLink = () => {
const previousUrl = editor.value?.getAttributes('link').href
// eslint-disable-next-line no-alert
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null)
return
// empty
if (url === '') {
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
const addImage = () => {
// eslint-disable-next-line no-alert
const url = window.prompt('URL')
if (url)
editor.value?.chain().focus().setImage({ src: url }).run()
}
const refVForm = ref()
const categoryTitle = ref()
const categorySlug = ref()
const categoryImg = ref()
const parentCategory = ref()
const parentStatus = ref()
const resetForm = () => {
emit('update:isDrawerOpen', false)
refVForm.value?.reset()
editor.value?.commands.clearContent()
}
</script>
<template>
<VNavigationDrawer
:model-value="props.isDrawerOpen"
temporary
location="end"
width="370"
class="category-navigation-drawer scrollable-content"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Add Category"
@cancel="$emit('update:isDrawerOpen', false)"
/>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<VForm
ref="refVForm"
@submit.prevent=""
>
<VRow>
<VCol cols="12">
<VTextField
v-model="categoryTitle"
label="Title"
:rules="[requiredValidator]"
placeholder="Fashion"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="categorySlug"
label="Slug"
:rules="[requiredValidator]"
placeholder="Trends fashion"
/>
</VCol>
<VCol cols="12">
<VFileInput
v-model="categoryImg"
prepend-icon=""
:rules="[requiredValidator]"
density="compact"
label="No file chosen"
clearable
>
<template #append>
<VBtn variant="outlined">
Choose
</VBtn>
</template>
</VFileInput>
</VCol>
<VCol cols="12">
<VSelect
v-model="parentCategory"
:rules="[requiredValidator]"
label="Parent Category"
placeholder="Select Parent Category"
:items="['HouseHold', 'Management', 'Electronics', 'Office', 'Accessories']"
/>
</VCol>
<VCol cols="12">
<div class="tiptap-editor-wrapper rounded py-2 px-4">
<EditorContent :editor="editor" />
<div
v-if="editor"
class="d-flex justify-end flex-wrap gap-x-2"
>
<VIcon
icon="ri-bold"
:color="editor.isActive('bold') ? 'primary' : ''"
size="20"
@click="editor.chain().focus().toggleBold().run()"
/>
<VIcon
:color="editor.isActive('underline') ? 'primary' : ''"
icon="ri-underline"
size="20"
@click="editor.commands.toggleUnderline()"
/>
<VIcon
:color="editor.isActive('italic') ? 'primary' : ''"
icon="ri-italic"
size="20"
@click="editor.chain().focus().toggleItalic().run()"
/>
<VIcon
:color="editor.isActive('bulletList') ? 'primary' : ''"
icon="ri-list-check"
size="20"
@click="editor.chain().focus().toggleBulletList().run()"
/>
<VIcon
:color="editor.isActive('orderedList') ? 'primary' : ''"
icon="ri-list-ordered-2"
size="20"
@click="editor.chain().focus().toggleOrderedList().run()"
/>
<VIcon
icon="ri-links-line"
size="20"
@click="setLink"
/>
<VIcon
icon="ri-image-line"
size="20"
@click="addImage"
/>
</div>
</div>
</VCol>
<VCol cols="12">
<VSelect
v-model="parentStatus"
:rules="[requiredValidator]"
placeholder="Select Category Status"
label="Parent Status"
:items="['Published', 'Inactive', 'Scheduled']"
/>
</VCol>
<VCol cols="12">
<div class="d-flex justify-start">
<VBtn
type="submit"
color="primary"
class="me-4"
>
Add
</VBtn>
<VBtn
color="error"
variant="outlined"
@click="resetForm"
>
Discard
</VBtn>
</div>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>
<style lang="scss">
.category-navigation-drawer {
.ProseMirror {
padding: 0.5rem;
block-size: auto;
min-block-size: 6.25rem;
p {
margin-block-end: 0;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
}
.is-active {
border-color: rgba(var(--v-theme-primary), var(--v-border-opacity)) !important;
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
color: rgb(var(--v-theme-primary));
}
.ProseMirror-focused{
outline: none !important;
}
.tiptap-editor-wrapper {
border: 1px solid rgba(var(--v-border-color), 0.22);
&:hover {
border-color: rgba(var(--v-border-color), 0.6);
}
}
}
</style>

View File

@@ -0,0 +1,197 @@
<script setup>
import rocketImg from '@images/eCommerce/rocket.png'
const props = defineProps({
customerData: {
type: null,
required: true,
},
})
const isUserInfoEditDialogVisible = ref(false)
const isUpgradePlanDialogVisible = ref(false)
</script>
<template>
<VRow>
<!-- SECTION Customer Details -->
<VCol cols="12">
<VCard v-if="props.customerData">
<VCardText class="text-center pt-15">
<!-- 👉 Avatar -->
<VAvatar
rounded
:size="120"
:color="!props.customerData.customer ? 'primary' : undefined"
:variant="!props.customerData.avatar ? 'tonal' : undefined"
>
<VImg
v-if="props.customerData.avatar"
:src="props.customerData.avatar"
/>
<span
v-else
class="text-5xl font-weight-medium"
>
User Not Availaable
</span>
</VAvatar>
<!-- 👉 Customer fullName -->
<h6 class="text-h5 mt-4">
{{ props.customerData.customer }}
</h6>
<p class="text-body-1 mb-0">
Customer ID #{{ props.customerData.customerId }}
</p>
<div class="d-flex justify-space-evenly gap-x-12 mt-6">
<div class="d-flex align-center">
<VAvatar
variant="tonal"
color="primary"
rounded
class="me-4"
>
<VIcon icon="ri-shopping-cart-line" />
</VAvatar>
<div class="d-flex flex-column align-start">
<h5 class="text-h5">
{{ props.customerData.order }}
</h5>
<span class="text-body-1">Orders</span>
</div>
</div>
<div class="d-flex align-center">
<VAvatar
variant="tonal"
color="primary"
rounded
class="me-4"
>
<VIcon icon="ri-money-dollar-circle-line" />
</VAvatar>
<div class="d-flex flex-column align-start">
<h5 class="text-h5">
{{ Math.round(props.customerData.totalSpent) }}
</h5>
<span class="text-body-1">Spent</span>
</div>
</div>
</div>
</VCardText>
<!-- 👉 Customer Details -->
<VCardText>
<h5 class="text-h5">
Details
</h5>
<VDivider class="my-4" />
<VList class="card-list mt-2">
<VListItem>
<VListItemTitle>
<span class="font-weight-medium me-2">Username:</span>
<span class="text-body-1">
{{ props.customerData.customer }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<span class="font-weight-medium me-2">Billing Email:</span>
<span class="text-body-1">
{{ props.customerData.email }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<span class="font-weight-medium me-2">Status:</span>
<span class="text-body-1">
{{ props.customerData.status }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<span class="font-weight-medium me-2">Contact:</span>
<span class="text-body-1">
{{ props.customerData.contact }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<span class="font-weight-medium me-2">Country:</span>
<span class="text-body-1">
{{ props.customerData.country }}
</span>
</VListItemTitle>
</VListItem>
</VList>
<div class="mt-6 text-center">
<VBtn
block
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
>
Edit Details
</VBtn>
</div>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
<!-- SECTION Upgrade to Premium -->
<VCol cols="12">
<VCard
flat
class="current-plan"
color="primary"
>
<VCardText>
<div class="d-flex align-center">
<div>
<h5 class="text-h5 text-white mb-4">
Upgrade to premium
</h5>
<p class="mb-6 text-wrap">
Upgrade customer to premium membership to access pro features.
</p>
</div>
<div>
<VImg
:src="rocketImg"
height="108"
width="108"
/>
</div>
</div>
<VBtn
color="#fff"
class="text-primary"
block
@click="isUpgradePlanDialogVisible = !isUpgradePlanDialogVisible"
>
Upgrade to Premium
</VBtn>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
</VRow>
<UserInfoEditDialog v-model:isDialogVisible="isUserInfoEditDialogVisible" />
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 0.5rem;
}
</style>

View File

@@ -0,0 +1,194 @@
<script setup>
const searchQuery = ref('')
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const headers = [
{
title: 'Order',
key: 'order',
},
{
title: 'Date',
key: 'date',
},
{
title: 'Status',
key: 'status',
},
{
title: 'Spent',
key: 'spent',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const resolveStatus = status => {
if (status === 'Delivered')
return { color: 'success' }
if (status === 'Out for Delivery')
return { color: 'primary' }
if (status === 'Ready to Pickup')
return { color: 'info' }
if (status === 'Dispatched')
return { color: 'warning' }
}
const {
data: ordersData,
execute: fetchOrders,
} = await useApi(createUrl('/apps/ecommerce/orders', {
query: {
q: searchQuery,
page,
itemsPerPage,
sortBy,
orderBy,
},
}))
const orders = computed(() => ordersData.value?.orders || [])
const totalOrder = computed(() => ordersData.value?.total || 0)
const deleteOrder = async id => {
await $api(`/apps/ecommerce/orders/${ id }`, { method: 'DELETE' })
fetchOrders()
}
</script>
<template>
<VCard>
<VCardText>
<div class="d-flex align-center justify-sm-space-between justify-start flex-wrap gap-4">
<div class="text-h5">
Orders placed
</div>
<VTextField
v-model="searchQuery"
placeholder="Search Order"
density="compact"
style=" max-inline-size: 250px; min-inline-size: 200px;"
/>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="headers"
:items="orders"
item-value="id"
:items-length="totalOrder"
class="text-no-wrap rounded-0"
@update:options="updateOptions"
>
<!-- Order ID -->
<template #item.order="{ item }">
<RouterLink :to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }">
#{{ item.order }}
</RouterLink>
</template>
<!-- Date -->
<template #item.date="{ item }">
{{ new Date(item.date).toDateString() }}
</template>
<!-- Status -->
<template #item.status="{ item }">
<VChip
size="small"
:color="resolveStatus(item.status)?.color"
>
{{ item.status }}
</VChip>
</template>
<!-- Spent -->
<template #item.spent="{ item }">
${{ item.spent }}
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn size="small">
<VIcon icon="ri-more-2-fill" />
<VMenu activator="parent">
<VList>
<VListItem value="view">
<RouterLink
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }"
class="text-high-emphasis"
>
View
</RouterLink>
</VListItem>
<VListItem
value="delete"
@click="deleteOrder(item.id)"
>
Delete
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
<!-- Pagination -->
<template #bottom>
<VDivider />
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
Rows Per Page:
<VSelect
v-model="itemsPerPage"
class="per-page-select"
variant="plain"
:items="[10, 20, 25, 50, 100]"
/>
</div>
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalOrder) }}
</p>
<div class="d-flex gap-x-2 align-center me-2">
<VBtn
class="flip-in-rtl"
icon="ri-arrow-left-s-line"
variant="text"
density="comfortable"
color="high-emphasis"
:disabled="page <= 1"
@click="page <= 1 ? page = 1 : page--"
/>
<VBtn
class="flip-in-rtl"
icon="ri-arrow-right-s-line"
density="comfortable"
variant="text"
color="high-emphasis"
:disabled="page >= Math.ceil(totalOrder / itemsPerPage)"
@click="page >= Math.ceil(totalOrder / itemsPerPage) ? page = Math.ceil(totalOrder / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,427 @@
<script setup>
import usFlag from '@images/icons/countries/us.png'
import americanExpress from '@images/icons/payments/img/american-express.png'
import mastercard from '@images/icons/payments/img/mastercard.png'
import visa from '@images/icons/payments/img/visa-light.png'
const show = ref([
true,
false,
false,
])
const paymentShow = ref([
true,
false,
false,
])
const isEditAddressDialogVisible = ref(false)
const isCardAddDialogVisible = ref(false)
const isNewEditAddressDialogVisible = ref(false)
const isNewCardAddDialogVisible = ref(false)
const currentCardDetails = {
number: '1234 5678 9012 3456',
name: 'John Doe',
expiry: '12/2028',
cvv: '123',
isPrimary: false,
type: '',
}
const editBillingData = {
firstName: 'Gertrude',
lastName: 'Jennings',
selectedCountry: 'USA',
addressLine1: '100 Water Plant Avenue',
addressLine2: 'Building 1303 Wake Island',
landmark: 'Near Wake Island',
contact: '+1(609) 933-44-22',
country: 'USA',
state: 'Queensland',
zipCode: 403114,
}
const addressData = [
{
title: 'Home',
subtitle: '23 Shatinon Mekalan',
owner: 'Violet Mendoza',
defaultAddress: true,
address: ` 23 Shatinon Mekalan,
<br>
Melbourne, VIC 3000,
<br>
LondonUK`,
},
{
title: 'Office',
subtitle: '45 Rocker Terrace',
owner: 'Violet Mendoza',
defaultAddress: false,
address: ` 45 Rocker Terrace,
<br>
Latheronwheel,
<br>
KW5 8NW, London,
<br>
UK`,
},
{
title: 'Family',
subtitle: '512 Water Plant',
owner: 'Violet Mendoza',
defaultAddress: false,
address: ` 512 Water Plant,
<br>
Melbourne, VIC 3000,
<br>
LondonUK`,
},
]
const paymentData = [
{
title: 'Mastercard',
subtitle: 'Expries Apr 2028',
isDefaultMethod: false,
image: mastercard,
},
{
title: 'American Express',
subtitle: 'Expries Apr 2028',
isDefaultMethod: false,
image: americanExpress,
},
{
title: 'Visa',
subtitle: '45 Roker Terrace',
isDefaultMethod: true,
image: visa,
},
]
</script>
<template>
<!-- eslint-disable vue/no-v-html -->
<!-- 👉 Address Book -->
<VCard class="mb-6">
<VCardText>
<div class="d-flex justify-space-between mb-5 flex-wrap align-center gap-y-4 gap-x-6">
<h5 class="text-h5">
Address Book
</h5>
<VBtn
variant="outlined"
size="small"
@click="isNewEditAddressDialogVisible = !isNewEditAddressDialogVisible"
>
Add new Address
</VBtn>
</div>
<template
v-for="(address, index) in addressData"
:key="index"
>
<div class="d-flex justify-space-between mb-3 gap-y-2 flex-wrap align-center">
<div class="d-flex align-center gap-x-2">
<IconBtn
density="comfortable"
@click="show[index] = !show[index]"
>
<VIcon
:icon="show[index] ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'"
class="flip-in-rtl text-high-emphasis"
/>
</IconBtn>
<div>
<div class="d-flex align-center mb-1">
<h6 class="text-h6 me-2">
{{ address.title }}
</h6>
<VChip
v-if="address.defaultAddress"
color="success"
size="small"
>
Default Address
</VChip>
</div>
<span class="text-body-1">{{ address.subtitle }}</span>
</div>
</div>
<div class="ms-11">
<IconBtn @click="isEditAddressDialogVisible = true">
<VIcon
icon="ri-edit-box-line"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="ri-delete-bin-7-line"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="ri-more-2-fill"
class="flip-in-rtl"
/>
</IconBtn>
</div>
</div>
<VExpandTransition>
<div
v-show="show[index]"
class="ps-12"
>
<div class="mb-1 font-weight-medium text-high-emphasis">
{{ address.owner }}
</div>
<div v-html="address.address" />
</div>
</VExpandTransition>
<VDivider
v-if="index !== addressData.length - 1"
class="my-3"
/>
</template>
</VCardText>
</VCard>
<!-- 👉 Payment Methods -->
<VCard>
<VCardText>
<div class="d-flex justify-space-between mb-5 flex-wrap align-center gap-y-4 gap-x-6">
<h5 class="text-h5">
Payment Methods
</h5>
<VBtn
variant="outlined"
size="small"
@click="isNewCardAddDialogVisible = !isNewCardAddDialogVisible"
>
Add Payment Methods
</VBtn>
</div>
<template
v-for="(payment, index) in paymentData"
:key="index"
>
<div class="d-flex justify-space-between mb-4 gap-y-2 flex-wrap align-center">
<div class="d-flex align-center gap-2">
<IconBtn
density="comfortable"
@click="paymentShow[index] = !paymentShow[index]"
>
<VIcon
:icon="paymentShow[index] ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'"
class="flip-in-rtl text-high-emphasis"
/>
</IconBtn>
<VImg
:src="payment.image"
height="30"
width="50"
class="me-4"
/>
<div>
<div class="d-flex flex-wrap mb-1">
<h6 class="text-h6 me-2">
{{ payment.title }}
</h6>
<VChip
v-if="payment.isDefaultMethod"
color="success"
density="comfortable"
>
Default Method
</VChip>
</div>
<span class="text-body-1">{{ payment.subtitle }}</span>
</div>
</div>
<div class="ms-11">
<IconBtn @click="isCardAddDialogVisible = true">
<VIcon
icon="ri-edit-box-line"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="ri-delete-bin-7-line"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="ri-more-2-fill"
class="flip-in-rtl"
/>
</IconBtn>
</div>
</div>
<VExpandTransition>
<div
v-show="paymentShow[index]"
class="ps-12"
>
<VRow>
<VCol
cols="12"
md="6"
>
<VTable>
<tr>
<td
class="text-sm pb-1"
style="inline-size: 100px;"
>
Name
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
Violet Mendoza
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Number
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
**** 4487
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Expires
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
08/2028
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Type
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
Master Card
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Issuer
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
VICBANK
</td>
</tr>
<tr>
<td class="text-sm pb-1">
ID
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
DH73DJ8
</td>
</tr>
</VTable>
</VCol>
<VCol
cols="12"
md="6"
>
<VTable>
<tr>
<td
class="text-sm pb-1"
style="inline-size: 100px;"
>
Billing
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
United Kingdom
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Number
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
+7634 983 637
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Email
</td>
<td class="text-sm text-high-emphasis font-weight-medium">
vafgot@vultukir.org
</td>
</tr>
<tr>
<td class="text-sm pb-1">
Origin
</td>
<td class="d-flex">
<div class="text-body-2 font-weight-medium text-high-emphasis me-2">
United States
</div>
<img
:src="usFlag"
height="20"
width="20"
>
</td>
</tr>
<tr>
<td class="text-sm pb-1">
CVC
</td>
<td class="d-flex">
<div class="text-body-2 font-weight-medium text-high-emphasis me-2">
Passed
</div>
<VAvatar
variant="tonal"
color="success"
size="20"
inline
>
<VIcon
icon="ri-check-line"
color="success"
size="12"
/>
</VAvatar>
</td>
</tr>
</VTable>
</VCol>
</VRow>
</div>
</VExpandTransition>
<VDivider
v-if="index !== paymentData.length - 1"
class="my-4"
/>
</template>
</VCardText>
</VCard>
<AddEditAddressDialog
v-model:isDialogVisible="isEditAddressDialogVisible"
:billing-address="editBillingData"
/>
<AddEditAddressDialog v-model:isDialogVisible="isNewEditAddressDialogVisible" />
<CardAddEditDialog
v-model:isDialogVisible="isCardAddDialogVisible"
:card-details="currentCardDetails"
/>
<CardAddEditDialog v-model:isDialogVisible="isNewCardAddDialogVisible" />
</template>

View File

@@ -0,0 +1,89 @@
<script setup>
const notifications = ref([
{
type: 'New for you',
email: true,
browser: false,
app: false,
},
{
type: 'Account activity',
email: false,
browser: true,
app: true,
},
{
type: 'A new browser used to sign in',
email: true,
browser: true,
app: true,
},
{
type: 'A new device is linked',
email: false,
browser: true,
app: false,
},
])
</script>
<template>
<VCard title="Notifications">
<VDivider />
<VCardText>
<h6 class="text-h6">
You will receive notification for the below selected items.
</h6>
</VCardText>
<VTable class="text-no-wrap rounded-0">
<thead>
<tr>
<th scope="col">
TYPE
</th>
<th scope="col">
EMAIL
</th>
<th scope="col">
BROWSER
</th>
<th scope="col">
APP
</th>
</tr>
</thead>
<tbody>
<tr
v-for="notification in notifications"
:key="notification.type"
>
<td class="text-high-emphasis">
{{ notification.type }}
</td>
<td>
<VCheckbox v-model="notification.email" />
</td>
<td>
<VCheckbox v-model="notification.browser" />
</td>
<td>
<VCheckbox v-model="notification.app" />
</td>
</tr>
</tbody>
</VTable>
<VDivider />
<VCardText class="d-flex flex-wrap gap-4">
<VBtn>Save changes</VBtn>
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,127 @@
<script setup>
import CustomerOrderTable from './CustomerOrderTable.vue'
</script>
<template>
<VRow class="match-height">
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText class="d-flex gap-y-2 flex-column">
<VAvatar
variant="tonal"
color="primary"
icon="ri-money-dollar-circle-line"
rounded
/>
<h6 class="text-lg font-weight-medium">
Account Balance
</h6>
<div class="text-base">
<p class="mb-0">
<span class="text-primary font-weight-medium text-lg me-1">$7480</span>
Credit Left
</p>
<p class="mb-0 text-base">
Account balance for next purchase
</p>
</div>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText class="d-flex gap-y-2 flex-column">
<VAvatar
variant="tonal"
color="success"
icon="ri-gift-line"
rounded
/>
<h6 class="text-lg font-weight-medium">
Loyalty Program
</h6>
<div>
<VChip
color="success"
size="small"
class="mb-2"
>
Platinum Member
</VChip>
<p class="mb-0 text-base">
3000 points to next tier
</p>
</div>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText class="d-flex gap-y-2 flex-column">
<VAvatar
variant="tonal"
color="warning"
icon="ri-star-smile-line"
rounded
/>
<h6 class="text-lg font-weight-medium">
Wishlist
</h6>
<div>
<p class=" mb-0">
<span class="text-warning font-weight-medium text-lg me-1">15</span>
items in wishlist
</p>
<p class="mb-0 text-base">
Receive notification when items go on sale
</p>
</div>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText class="d-flex gap-y-2 flex-column">
<VAvatar
variant="tonal"
color="info"
icon="ri-vip-crown-line"
rounded
/>
<h6 class="text-lg font-weight-medium">
Coupons
</h6>
<div>
<p class="mb-0">
<span class="text-info text-lg me-2">21</span>
Coupons you win
</p>
<p class="mb-0 text-base">
Use coupon on next purchase
</p>
</div>
</VCardText>
</VCard>
</VCol>
<VCol>
<CustomerOrderTable />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,201 @@
<script setup>
import chrome from '@images/logos/chrome.png'
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
const smsVerificationNumber = ref('+1(968) 819-2547')
const isTwoFactorDialogOpen = ref(false)
const recentDeviceHeader = [
{
title: 'BROWSER',
key: 'browser',
},
{
title: 'DEVICE',
key: 'device',
},
{
title: 'LOCATION',
key: 'location',
},
{
title: 'RECENT ACTIVITY',
key: 'activity',
},
]
const recentDevices = [
{
browser: 'Chrome on Windows',
logo: chrome,
device: 'Dell XPS 15',
location: 'United States',
activity: '10, Jan 2020 20:07',
},
{
browser: 'Chrome on Android',
logo: chrome,
device: 'Google Pixel 3a',
location: 'Ghana',
activity: '11, Jan 2020 10:16',
},
{
browser: 'Chrome on macOS',
logo: chrome,
device: 'Apple iMac',
location: 'Mayotte',
activity: '11, Jan 2020 12:10',
},
{
browser: 'Chrome on iPhone',
logo: chrome,
device: 'Apple iPhone XR',
location: 'Mauritania',
activity: '12, Jan 2020 8:29',
},
]
</script>
<template>
<VRow>
<VCol cols="12">
<!-- 👉 Change password -->
<VCard title="Change Password">
<VCardText>
<VAlert
variant="tonal"
color="warning"
closable
class="mb-4"
>
<VAlertTitle>Ensure that these requirements are met</VAlertTitle>
<span>Minimum 8 characters long, uppercase & symbol</span>
</VAlert>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
label="New Password"
placeholder="············"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Confirm Password"
placeholder="············"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
<VCol cols="12">
<VBtn type="submit">
Change Password
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Two step verification -->
<VCard
title="Two-step verification"
subtitle="Keep your account secure with authentication step."
>
<VCardText>
<div>
<h4 class="font-weight-medium mb-1">
SMS
</h4>
<VTextField
density="compact"
variant="outlined"
:model-value="smsVerificationNumber"
readonly
>
<template #append>
<VBtn
icon
rounded
variant="outlined"
color="secondary"
class="me-2"
>
<VIcon
icon="ri-edit-box-line"
size="24"
@click="isTwoFactorDialogOpen = true"
/>
</VBtn>
<VBtn
icon
rounded
variant="outlined"
color="secondary"
>
<VIcon
size="24"
icon="ri-user-add-line"
/>
</VBtn>
</template>
</VTextField>
</div>
<p class="mb-0 mt-4">
Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a
href="javascript:void(0)"
class="text-decoration-none"
>Learn more</a>.
</p>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Recent devices -->
<VCard title="Recent devices">
<VDataTable
:items="recentDevices"
:headers="recentDeviceHeader"
hide-default-footer
class="text-no-wrap rounded-0"
>
<template #item.browser="{ item }">
<div class="d-flex text-high-emphasis">
<VAvatar
:image="item.logo"
:size="22"
class="me-4"
/>
{{ item.browser }}
</div>
</template>
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
<template #bottom />
</VDataTable>
</VCard>
</VCol>
</VRow>
<!-- 👉 Enable One Time Password Dialog -->
<TwoFactorAuthDialog
v-model:isDialogVisible="isTwoFactorDialogOpen"
:sms-code="smsVerificationNumber"
/>
</template>

View File

@@ -0,0 +1,130 @@
<script setup>
const contactMethod = ref('Phone number')
const fullName = ref('Only require last name')
const companyName = ref('Don\'t include')
const addressLine = ref('Optional')
const shippingAddress = ref('Optional')
</script>
<template>
<VCard
title="Customer contact method"
subtitle="Select what contact method customers use to check out."
class="mb-6"
>
<VCardText>
<VRadioGroup
v-model="contactMethod"
class="mb-4"
>
<VRadio
label="Phone number"
value="Phone number"
/>
<VRadio
label="Email"
value="Email"
/>
</VRadioGroup>
<VAlert
color="warning"
variant="tonal"
icon="ri-information-line"
>
<VAlertTitle class="mb-0">
To send SMS updates, you need to install an SMS App.
</VAlertTitle>
</VAlert>
</VCardText>
</VCard>
<VCard
title="Customer information"
class="mb-6"
>
<VCardText>
<VRadioGroup
v-model="fullName"
label="Full name"
class="mb-4"
>
<VRadio
value="Only require last name"
label="Only require last name"
/>
<VRadio
value="Require first and last name"
label="Require first and last name"
/>
</VRadioGroup>
<VRadioGroup
v-model="companyName"
label="Company name"
class="mb-4"
>
<VRadio
value="Don't include"
label="Don't include"
/>
<VRadio
value="Optional"
label="Optional"
/>
<VRadio
value="Required"
label="Required"
/>
</VRadioGroup>
<VRadioGroup
v-model="addressLine"
label="Address line 2 (apartment, unit, etc.)"
class="mb-4"
>
<VRadio
value="Don't include"
label="Don't include"
/>
<VRadio
value="Optional"
label="Optional"
/>
<VRadio
value="Required"
label="Required"
/>
</VRadioGroup>
<VRadioGroup
v-model="shippingAddress"
label="Shipping address phone number"
class="mb-4"
>
<VRadio
value="Don't include"
label="Don't include"
/>
<VRadio
value="Optional"
label="Optional"
/>
<VRadio
value="Required"
label="Required"
/>
</VRadioGroup>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1,119 @@
<template>
<div>
<VCard
title="Location Name"
class="mb-6"
>
<VCardText>
<VTextField
label="Location Name"
placeholder="Empire Hub"
/>
<div class="my-4">
<VCheckbox label="Fulfil online orders from this location" />
</div>
<VAlert
color="info"
variant="tonal"
>
<template #prepend>
<VAvatar
size="28"
icon="ri-information-line"
variant="elevated"
color="info"
rounded
/>
</template>
<VAlertTitle class="mb-0">
This is your default location. To change whether you fulfill online orders from this location, select another default location first.
</VAlertTitle>
</VAlert>
</VCardText>
</VCard>
<VCard title="Address">
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
label="Country/religion"
placeholder="Select Country"
:items="['United States', 'UK', 'Canada']"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="Address"
placeholder="123 , New Street"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="Apartment, suite, etc."
placeholder="Empire Heights"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="Phone"
placeholder="+1 (234) 456-7890"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="City"
placeholder="New York"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="State"
placeholder="NY"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="PIN code"
placeholder="123897"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4 mt-6">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</div>
</template>

View File

@@ -0,0 +1,192 @@
<script setup>
const customerNotifications = ref([
{
type: 'New customer sign up',
email: true,
app: false,
},
{
type: 'Customer account password reset',
email: false,
app: true,
},
{
type: 'Customer account invite',
email: false,
app: false,
},
])
const shippingNotifications = ref([
{
type: 'Picked up',
email: true,
app: false,
},
{
type: 'Shipping update ',
email: false,
app: true,
},
{
type: 'Delivered',
email: false,
app: false,
},
])
const ordersNotification = ref([
{
type: 'Order purchase',
email: true,
app: false,
},
{
type: 'Order cancelled',
email: false,
app: true,
},
{
type: 'Order refund request',
email: false,
app: false,
},
{
type: 'Order confirmation',
email: false,
app: true,
},
{
type: 'Payment error',
email: false,
app: true,
},
])
</script>
<template>
<VCard class="mb-4">
<VCardText>
<h5 class="text-h5 mb-4">
Customer
</h5>
<VTable class="text-no-wrap text-high-emphasis border rounded mb-6">
<thead>
<tr>
<th scope="col">
TYPE
</th>
<th scope="col">
EMAIL
</th>
<th scope="col">
APP
</th>
</tr>
</thead>
<tbody>
<tr
v-for="notification in customerNotifications"
:key="notification.type"
>
<td width="400px">
{{ notification.type }}
</td>
<td>
<VCheckbox v-model="notification.email" />
</td>
<td>
<VCheckbox v-model="notification.app" />
</td>
</tr>
</tbody>
</VTable>
<h5 class="text-h5 mb-4">
Orders
</h5>
<VTable class="border rounded text-high-emphasis text-no-wrap mb-6">
<thead>
<tr>
<th scope="col">
TYPE
</th>
<th scope="col">
EMAIL
</th>
<th scope="col">
APP
</th>
</tr>
</thead>
<tbody>
<tr
v-for="notification in ordersNotification"
:key="notification.type"
>
<td width="400px">
{{ notification.type }}
</td>
<td>
<VCheckbox v-model="notification.email" />
</td>
<td>
<VCheckbox v-model="notification.app" />
</td>
</tr>
</tbody>
</VTable>
<h5 class="text-h5 mb-4">
Shipping
</h5>
<VTable class="border rounded text-high-emphasis text-no-wrap mb-6">
<thead>
<tr>
<th scope="col">
TYPE
</th>
<th scope="col">
EMAIL
</th>
<th scope="col">
APP
</th>
</tr>
</thead>
<tbody>
<tr
v-for="notification in shippingNotifications"
:key="notification.type"
>
<td width="400px">
{{ notification.type }}
</td>
<td>
<VCheckbox v-model="notification.email" />
</td>
<td>
<VCheckbox v-model="notification.app" />
</td>
</tr>
</tbody>
</VTable>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1,172 @@
<script setup>
import { ref } from 'vue'
import paypal from '@images/cards/paypal-primary.png'
const isAddPaymentMethodsDialogVisible = ref(false)
const isPaymentProvidersDialogVisible = ref(false)
</script>
<template>
<div>
<!-- 👉 Payment Providers -->
<VCard
class="mb-6"
title="Payment providers"
>
<VCardText>
<p>
Providers that enable you to accept payment methods at a rate set by the third-party. An additional fee will apply to new orders once you select a plan.
</p>
<VBtn
variant="outlined"
@click="isPaymentProvidersDialogVisible = !isPaymentProvidersDialogVisible"
>
Choose a provider
</VBtn>
</VCardText>
</VCard>
<!-- 👉 Supported Payment Methods -->
<VCard
title="Supported payment methods"
subtitle="Payment methods that are available with one of Vuexy's approved payment providers."
class="mb-6"
>
<VCardText>
<h6 class="text-h6 mb-5">
Default
</h6>
<div class="rounded bg-var-theme-background pa-5 mb-6">
<div class="d-flex justify-space-between align-center mb-6">
<VAvatar
variant="elevated"
color="#ffffff"
rounded
class="px-1"
>
<VImg
:src="paypal"
height="21"
width="21"
/>
</VAvatar>
<VBtn variant="text">
Activate PayPal
</VBtn>
</div>
<VDivider />
<div class="d-flex justify-space-between flex-wrap mt-6 gap-x-4">
<div>
<div class="text-body-2 mb-2">
Provider
</div>
<h6 class="text-h6">
PayPal
</h6>
</div>
<div>
<div class="text-body-2 mb-2">
Status
</div>
<VChip
color="warning"
size="small"
>
Inactive
</VChip>
</div>
<div>
<div class="text-body-2 mb-2">
Transaction Fee
</div>
<h6 class="text-h6">
2.99%
</h6>
</div>
</div>
</div>
<VBtn
variant="outlined"
@click="isAddPaymentMethodsDialogVisible = !isAddPaymentMethodsDialogVisible"
>
Add Payment Method
</VBtn>
</VCardText>
</VCard>
<!-- 👉 Manual Payment Methods -->
<VCard
title="Manual payment methods"
class="mb-6"
>
<VCardText>
<p>Payments that are made outside your online store. When a customer selects a manual payment method such as cash on delivery, you'll need to approve their order before it can be fulfilled.</p>
<VBtnGroup
v-show="$vuetify.display.smAndUp"
divided
density="compact"
variant="outlined"
color="primary"
>
<VBtn>
Add Manual Payment Methods
</VBtn>
<VBtn>
<VIcon
size="20"
icon="ri-arrow-down-s-line"
/>
<VMenu activator="parent">
<VList>
<VListItem
v-for="(item, index) in ['Create custom payment method', 'Bank Deposit', 'Money Order', 'Cash on Delivery(COD)']"
:key="index"
:value="index"
>
<VListItemTitle>{{ item }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</VBtnGroup>
<VBtn
variant="outlined"
class="d-block d-sm-none"
>
Add Manual Payment Methods
<VMenu activator="parent">
<VList>
<VListItem
v-for="(item, index) in ['Create custom payment method', 'Bank Deposit', 'Money Order', 'Cash on Delivery(COD)']"
:key="index"
:value="index"
>
<VListItemTitle>{{ item }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</div>
<AddPaymentMethodDialog v-model:is-dialog-visible="isAddPaymentMethodsDialogVisible" />
<PaymentProvidersDialog v-model:is-dialog-visible="isPaymentProvidersDialogVisible" />
</template>

View File

@@ -0,0 +1,193 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import americaFlag from '@images/icons/countries/us.png'
const domesticTableData = [
{
rate: 'Weight',
condition: '5Kg-10Kg',
price: '$9',
},
{
rate: 'VAT',
condition: '12%',
price: '$25',
},
{
rate: 'Duty',
condition: '-',
price: '-',
},
]
const InternationalTableData = [
{
rate: 'Weight',
condition: '5Kg-10Kg',
price: '$9',
},
{
rate: 'VAT',
condition: '12%',
price: '$25',
},
{
rate: 'Duty',
condition: 'Japan',
price: '$49',
},
]
</script>
<template>
<VCard class="mb-6">
<VCardItem
title="Shipping Zone"
subtitle="Choose where you ship and how much you charge for shipping at checkout."
>
<template #append>
<VBtn variant="text">
Create Zone
</VBtn>
</template>
</VCardItem>
<VCardText>
<div class="mb-6">
<div class="d-flex flex-wrap align-center mb-4">
<VAvatar
:image="avatar1"
size="34"
class="me-4"
/>
<div>
<h6 class="text-h6">
Domestic
</h6>
<p class="text-body-2 mb-0">
United state of America
</p>
</div>
<VSpacer />
<div>
<IconBtn size="large">
<VIcon icon="ri-pencil-line" />
</IconBtn>
<IconBtn size="large">
<VIcon icon="ri-delete-bin-7-line" />
</IconBtn>
</div>
</div>
<VTable class="border rounded mb-4">
<thead>
<tr>
<th>RATE NAME</th>
<th>CONDITION</th>
<th>PRICE</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<tr
v-for="(data, index) in domesticTableData"
:key="index"
>
<td>{{ data.rate }}</td>
<td>{{ data.condition }}</td>
<td>{{ data.price }}</td>
<td style="inline-size: 2rem;">
<IconBtn>
<VIcon icon="ri-more-2-line" />
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
<VBtn
variant="outlined"
size="small"
>
Add rate
</VBtn>
</div>
<div>
<div class="d-flex flex-wrap align-center mb-4">
<VAvatar
:image="americaFlag"
size="34"
class="me-4"
/>
<div>
<h6 class="text-h6">
International
</h6>
<p class="text-body-2 mb-0">
United state of America
</p>
</div>
<VSpacer />
<div>
<IconBtn size="large">
<VIcon icon="ri-pencil-line" />
</IconBtn>
<IconBtn size="large">
<VIcon icon="ri-delete-bin-7-line" />
</IconBtn>
</div>
</div>
<VTable class="border rounded mb-4">
<thead>
<tr>
<th>RATE NAME</th>
<th>CONDITION</th>
<th>PRICE</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<tr
v-for="(data, index) in InternationalTableData"
:key="index"
>
<td>{{ data.rate }}</td>
<td>{{ data.condition }}</td>
<td>{{ data.price }}</td>
<td style="inline-size: 2rem;">
<IconBtn>
<VIcon icon="ri-more-2-line" />
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
<VBtn
variant="outlined"
size="small"
>
Add rate
</VBtn>
</div>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1,236 @@
<template>
<VCard
title="Profile"
class="mb-6"
>
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
label="Store name"
placeholder="ABCD"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Phone"
placeholder="+(123) 456-7890"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Store contact email"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Sender email"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol>
<VAlert
color="warning"
variant="tonal"
icon="ri-notification-3-line"
>
<VAlertTitle class="mb-0">
Confirm that you have access to johndoe@gmail.com in sender email settings.
</VAlertTitle>
</VAlert>
</VCol>
</VRow>
</VCardText>
</VCard>
<VCard
title="Billing Information"
class="mb-6"
>
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
label="Legal business name"
placeholder="Themeselection"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
label="Country*"
:items="['United States', 'Canada', 'UK']"
placeholder="Canada"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
placeholder="126, New Street"
label="Address"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Apartment,suit, etc."
placeholder="Empire Heights"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="City"
placeholder="New York"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="State"
placeholder="NY"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
label="PIN Code"
placeholder="111011"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<VCard
title="Time zone and units of measurement"
subtitle="Used to calculate product prices, shipping weights, and order times."
class="mb-6"
>
<VCardText>
<VRow>
<VCol cols="12">
<VSelect
label="Time zone"
:items="['(UTC-12:00) International Date Line West', '(UTC-11:00) Coordinated Universal Time-11', '(UTC-09:00) Alaska', '(UTC-08:00) Baja California']"
placeholder="(UTC-12:00) International Date Line West"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
label="Unit system"
:items="['Metric System', 'Imperial', 'International System']"
placeholder="Metric System"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSelect
label="Default weight unit"
placeholder="Kilogram"
:items="['Kilogram', 'Pounds', 'Gram']"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<VCard
title="Store currency"
subtitle="The currency your products are sold in."
class="mb-6"
>
<VCardText>
<VSelect
label="Store currency"
:items="['USD', 'INR', 'Euro', 'Pound']"
placeholder="USD"
/>
</VCardText>
</VCard>
<VCard
title="Order id format"
subtitle="Shown on the Orders page, customer pages, and customer order notifications to identify orders."
class="mb-6"
>
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
label="Prefix"
prefix="#"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Suffix"
suffix="$"
/>
</VCol>
</VRow>
<div class="mt-2">
Your order ID will appear as #1001, #1002, #1003 ...
</div>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,302 @@
<script setup>
import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Underline } from '@tiptap/extension-underline'
import { StarterKit } from '@tiptap/starter-kit'
import {
EditorContent,
useEditor,
} from '@tiptap/vue-3'
const emit = defineEmits(['close'])
const to = ref('')
const cc = ref('')
const bcc = ref('')
const subject = ref('')
const message = ref('')
const emailCc = ref(false)
const emailBcc = ref(false)
const resetValues = () => {
to.value = subject.value = message.value = ''
}
const editor = useEditor({
content: '',
extensions: [
StarterKit,
Image,
Placeholder.configure({ placeholder: 'Message' }),
Underline,
Link.configure({ openOnClick: false }),
],
})
const setLink = () => {
const previousUrl = editor.value?.getAttributes('link').href
// eslint-disable-next-line no-alert
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null)
return
// empty
if (url === '') {
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
const addImage = () => {
// eslint-disable-next-line no-alert
const url = window.prompt('URL')
if (url)
editor.value?.chain().focus().setImage({ src: url }).run()
}
</script>
<template>
<VCard
class="email-compose-dialog"
elevation="24"
max-width="30vw"
>
<VCardItem class="py-3">
<VCardTitle class="text-medium-emphasis">
Compose Mail
</VCardTitle>
<template #append>
<IconBtn @click="$emit('close')">
<VIcon icon="ri-subtract-line" />
</IconBtn>
<IconBtn @click="$emit('close'); resetValues()">
<VIcon icon="ri-close-line" />
</IconBtn>
</template>
</VCardItem>
<div class="pe-5">
<VTextField
v-model="to"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
To:
</div>
</template>
<template #append>
<span class="cursor-pointer text-medium-emphasis">
<span @click="emailCc = !emailCc">Cc</span>
<span class="mx-1">|</span>
<span @click="emailBcc = !emailBcc">Bcc</span>
</span>
</template>
</VTextField>
</div>
<VExpandTransition>
<div v-if="emailCc">
<VDivider />
<VTextField
v-model="cc"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
Cc:
</div>
</template>
</VTextField>
</div>
</VExpandTransition>
<VExpandTransition>
<div v-if="emailBcc">
<VDivider />
<VTextField
v-model="bcc"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
Bcc:
</div>
</template>
</VTextField>
</div>
</VExpandTransition>
<VDivider />
<VTextField
v-model="subject"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
Subject:
</div>
</template>
</VTextField>
<VDivider />
<!-- 👉 Tiptap editor -->
<div class="tiptap-editor-wrapper">
<div
v-if="editor"
class="d-flex flex-wrap gap-x-1 px-4 py-2"
>
<IconBtn
rounded
:color="editor.isActive('bold') ? 'primary' : ''"
:variant="editor.isActive('bold') ? 'tonal' : 'text'"
@click="editor.chain().focus().toggleBold().run()"
>
<VIcon icon="ri-bold" />
</IconBtn>
<IconBtn
rounded
:color="editor.isActive('underline') ? 'primary' : ''"
:variant="editor.isActive('underline') ? 'tonal' : 'text'"
@click="editor.commands.toggleUnderline()"
>
<VIcon icon="ri-underline" />
</IconBtn>
<IconBtn
rounded
:color="editor.isActive('italic') ? 'primary' : ''"
:variant="editor.isActive('italic') ? 'tonal' : 'text'"
@click="editor.chain().focus().toggleItalic().run()"
>
<VIcon icon="ri-italic" />
</IconBtn>
<IconBtn
rounded
:color="editor.isActive('bulletList') ? 'primary' : ''"
:variant="editor.isActive('bulletList') ? 'tonal' : 'text'"
@click="editor.chain().focus().toggleBulletList().run()"
>
<VIcon icon="ri-list-check" />
</IconBtn>
<IconBtn
rounded
:color="editor.isActive('orderedList') ? 'primary' : ''"
:variant="editor.isActive('orderedList') ? 'tonal' : 'text'"
@click="editor.chain().focus().toggleOrderedList().run()"
>
<VIcon icon="ri-list-ordered-2" />
</IconBtn>
<IconBtn
rounded
@click="setLink"
>
<VIcon icon="ri-links-line" />
</IconBtn>
<IconBtn
rounded
@click="addImage"
>
<VIcon icon="ri-image-line" />
</IconBtn>
</div>
<VDivider />
<div class="mx-5">
<EditorContent :editor="editor" />
</div>
</div>
<div class="d-flex align-center px-5 py-4 gap-4">
<VBtn append-icon="ri-send-plane-line">
Send
<VMenu activator="parent">
<VList :items="['Schedule Mail', 'Save Draft']" />
</VMenu>
</VBtn>
<IconBtn>
<VIcon icon="ri-attachment-2" />
</IconBtn>
<VSpacer />
<IconBtn>
<VIcon icon="ri-more-2-line" />
</IconBtn>
<IconBtn @click="$emit('close'); resetValues()">
<VIcon icon="ri-delete-bin-7-line" />
</IconBtn>
</div>
</VCard>
</template>
<style lang="scss">
.email-compose-dialog {
z-index: 910 !important;
.v-field--prepended {
padding-inline-start: 20px;
}
.v-card-item {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
}
.v-textarea .v-field {
--v-field-padding-start: 20px;
}
.v-field__outline {
display: none;
}
.ProseMirror {
block-size: 150px;
overflow-y: auto;
padding-block: .5rem;
&:focus-visible {
outline: none;
}
p {
margin-block-end: 0;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
ul,ol{
padding-inline: 1.125rem;
}
}
}
</style>

View File

@@ -0,0 +1,213 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const emit = defineEmits(['toggleComposeDialogVisibility'])
defineOptions({
inheritAttrs: false,
})
const folders = [
{
title: 'Inbox',
prependIcon: 'ri-mail-line',
to: { name: 'apps-email' },
badge: {
content: '21',
color: 'primary',
},
},
{
title: 'Sent',
prependIcon: 'ri-send-plane-line',
to: {
name: 'apps-email-filter',
params: { filter: 'sent' },
},
},
{
title: 'Draft',
prependIcon: 'ri-edit-box-line',
to: {
name: 'apps-email-filter',
params: { filter: 'draft' },
},
badge: {
content: '2',
color: 'warning',
},
},
{
title: 'Starred',
prependIcon: 'ri-star-line',
to: {
name: 'apps-email-filter',
params: { filter: 'starred' },
},
},
{
title: 'Spam',
prependIcon: 'ri-spam-2-line',
to: {
name: 'apps-email-filter',
params: { filter: 'spam' },
},
badge: {
content: '4',
color: 'error',
},
},
{
title: 'Trash',
prependIcon: 'ri-delete-bin-7-line',
to: {
name: 'apps-email-filter',
params: { filter: 'trashed' },
},
},
]
const labels = [
{
title: 'Personal',
color: 'success',
to: {
name: 'apps-email-label',
params: { label: 'personal' },
},
},
{
title: 'Company',
color: 'primary',
to: {
name: 'apps-email-label',
params: { label: 'company' },
},
},
{
title: 'Important',
color: 'warning',
to: {
name: 'apps-email-label',
params: { label: 'important' },
},
},
{
title: 'Private',
color: 'error',
to: {
name: 'apps-email-label',
params: { label: 'private' },
},
},
]
</script>
<template>
<div class="d-flex flex-column h-100">
<!-- 👉 Compose -->
<div class="pa-5">
<VBtn
block
@click="$emit('toggleComposeDialogVisibility')"
>
Compose
</VBtn>
</div>
<!-- 👉 Folders -->
<PerfectScrollbar
:options="{ wheelPropagation: false }"
class="h-100 pt-4"
>
<ul class="email-filters-labels">
<RouterLink
v-for="folder in folders"
:key="folder.title"
v-slot="{ isActive, href, navigate }"
class="d-flex align-center cursor-pointer"
:to="folder.to"
custom
>
<li
v-bind="$attrs"
:href="href"
:class="isActive && 'email-filter-active text-primary'"
class="d-flex align-center cursor-pointer"
@click="navigate"
>
<VIcon
:icon="folder.prependIcon"
class="me-2"
size="20"
/>
<span>{{ folder.title }}</span>
<VSpacer />
<VChip
v-if="folder.badge?.content"
size="x-small"
:color="folder.badge.color"
>
{{ folder.badge.content }}
</VChip>
</li>
</RouterLink>
<!-- 👉 Labels -->
<li class="text-sm d-block text-uppercase text-disabled mt-9 mb-4">
LABELS
</li>
<RouterLink
v-for="label in labels"
:key="label.title"
v-slot="{ isActive, href, navigate }"
class="d-flex align-center"
:to="label.to"
custom
>
<li
v-bind="$attrs"
:href="href"
:class="isActive && 'email-label-active text-primary'"
class="cursor-pointer"
@click="navigate"
>
<VIcon
:color="label.color"
icon="ri-circle-fill"
size="12"
class="me-2"
/>
<span>{{ label.title }}</span>
</li>
</RouterLink>
</ul>
</PerfectScrollbar>
</div>
</template>
<style lang="scss">
.email-filters-labels {
> li {
position: relative;
margin-block-end: 4px;
padding-block: 4px;
padding-inline: 20px;
}
.email-filter-active,
.email-label-active {
&::after {
position: absolute;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 3px;
inset-block-start: 0;
inset-inline-start: 0;
}
}
}
</style>

View File

@@ -0,0 +1,487 @@
<script setup>
import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Underline } from '@tiptap/extension-underline'
import { StarterKit } from '@tiptap/starter-kit'
import {
EditorContent,
useEditor,
} from '@tiptap/vue-3'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useEmail } from '@/views/apps/email/useEmail'
const props = defineProps({
email: {
type: null,
required: true,
},
emailMeta: {
type: Object,
required: true,
},
})
const emit = defineEmits([
'refresh',
'navigated',
'close',
'trash',
'unread',
'read',
'star',
'unstar',
])
const { updateEmailLabels } = useEmail()
const { labels, resolveLabelColor, emailMoveToFolderActions, shallShowMoveToActionFor, moveSelectedEmailTo } = useEmail()
const handleMoveMailsTo = action => {
moveSelectedEmailTo(action, [props.email.id])
emit('refresh')
emit('close')
}
const updateMailLabel = async label => {
await updateEmailLabels([props.email.id], label)
emit('refresh')
}
const editor = useEditor({
content: '',
extensions: [
StarterKit,
Image,
Placeholder.configure({ placeholder: 'Write a Comment...' }),
Underline,
Link.configure({ openOnClick: false }),
],
})
const setLink = () => {
const previousUrl = editor.value?.getAttributes('link').href
// eslint-disable-next-line no-alert
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null)
return
// empty
if (url === '') {
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}
const addImage = () => {
// eslint-disable-next-line no-alert
const url = window.prompt('URL')
if (url)
editor.value?.chain().focus().setImage({ src: url }).run()
}
</script>
<template>
<!-- calc(100% - 256px) => 265px is left sidebar width -->
<VNavigationDrawer
temporary
:model-value="!!props.email"
location="right"
:scrim="false"
floating
class="email-view"
>
<template v-if="props.email">
<!-- 👉 header -->
<div class="email-view-header d-flex align-center px-5 py-4">
<IconBtn
class="me-2 flip-in-rtl"
@click="$emit('close')"
>
<VIcon icon="ri-arrow-left-s-line" />
</IconBtn>
<div class="d-flex align-center flex-wrap flex-grow-1 overflow-hidden gap-2">
<h6 class="text-h6 font-weight-regular text-truncate">
{{ props.email.subject }}
</h6>
<div class="d-flex flex-wrap gap-2">
<VChip
v-for="label in props.email.labels"
:key="label"
:color="resolveLabelColor(label)"
size="small"
class="text-capitalize flex-shrink-0"
>
{{ label }}
</VChip>
</div>
</div>
<div class="d-flex align-center gap-2">
<IconBtn
variant="plain"
:disabled="!props.emailMeta.hasPreviousEmail"
class="flip-in-rtl"
@click="$emit('navigated', 'previous')"
>
<VIcon icon="ri-arrow-left-s-line" />
</IconBtn>
<IconBtn
variant="plain"
class="flip-in-rtl"
:disabled="!props.emailMeta.hasNextEmail"
@click="$emit('navigated', 'next')"
>
<VIcon icon="ri-arrow-right-s-line" />
</IconBtn>
</div>
</div>
<VDivider />
<!-- 👉 Action bar -->
<div class="email-view-action-bar d-flex align-center text-medium-emphasis gap-1 px-5">
<!-- Trash -->
<IconBtn
v-show="!props.email.isDeleted"
@click="$emit('trash'); $emit('close')"
>
<VIcon icon="ri-delete-bin-7-line" />
<VTooltip
activator="parent"
location="top"
>
Delete Mail
</VTooltip>
</IconBtn>
<!-- Read/Unread -->
<IconBtn @click.stop="$emit('unread'); $emit('close')">
<VIcon icon="ri-mail-line" />
<VTooltip
activator="parent"
location="top"
>
Mark as Unread
</VTooltip>
</IconBtn>
<!-- Move to folder -->
<IconBtn>
<VIcon icon="ri-folder-line" />
<VTooltip
activator="parent"
location="top"
>
Move to
</VTooltip>
<VMenu activator="parent">
<VList density="compact">
<template
v-for="moveTo in emailMoveToFolderActions"
:key="moveTo.title"
>
<VListItem
:class="shallShowMoveToActionFor(moveTo.action) ? 'd-flex' : 'd-none'"
class="align-center"
href="#"
@click="handleMoveMailsTo(moveTo.action)"
>
<template #prepend>
<VIcon
:icon="moveTo.icon"
class="me-2"
size="20"
/>
</template>
<VListItemTitle class="text-capitalize">
{{ moveTo.action }}
</VListItemTitle>
</VListItem>
</template>
</VList>
</VMenu>
</IconBtn>
<!-- Update labels -->
<IconBtn>
<VIcon icon="ri-price-tag-3-line" />
<VTooltip
activator="parent"
location="top"
>
Label
</VTooltip>
<VMenu activator="parent">
<VList density="compact">
<VListItem
v-for="label in labels"
:key="label.title"
href="#"
@click.stop="updateMailLabel(label.title)"
>
<template #prepend>
<VBadge
inline
:color="resolveLabelColor(label.title)"
dot
/>
</template>
<VListItemTitle class="ms-2 text-capitalize">
{{ label.title }}
</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
<VSpacer />
<!-- Star/Unstar -->
<IconBtn
:color="props.email.isStarred ? 'warning' : 'default'"
@click="props.email?.isStarred ? $emit('unstar') : $emit('star')"
>
<VIcon icon="ri-star-line" />
</IconBtn>
<!-- Dots vertical -->
<MoreBtn />
</div>
<VDivider />
<!-- 👉 Mail Content -->
<PerfectScrollbar
tag="div"
class="mail-content-container flex-grow-1"
:options="{ wheelPropagation: false }"
>
<VCard class="ma-6 mb-4">
<VCardText class="mail-header">
<div class="d-flex align-start">
<VAvatar
size="38"
class="me-3"
>
<VImg
:src="props.email.from.avatar"
:alt="props.email.from.name"
/>
</VAvatar>
<div class="d-flex flex-wrap flex-grow-1 overflow-hidden">
<div class="text-truncate">
<h6 class="text-h6 font-weight-regular text-truncate">
{{ props.email.from.name }}
</h6>
<p class="text-body-2 mb-0">
{{ props.email.from.email }}
</p>
</div>
<VSpacer />
<div class="d-flex align-center">
<span class="text-disabled me-4">{{ formatDate(props.email.time) }}</span>
<IconBtn v-show="props.email.attachments.length">
<VIcon icon="ri-attachment-2" />
</IconBtn>
</div>
</div>
<MoreBtn class="align-self-sm-center" />
</div>
</VCardText>
<VDivider />
<VCardText>
<!-- eslint-disable vue/no-v-html -->
<div
class="text-base"
v-html="props.email.message"
/>
<!-- eslint-enable -->
</VCardText>
<template v-if="props.email.attachments.length">
<VDivider />
<VCardText class="d-flex flex-column gap-y-4">
<span>Attachments</span>
<div
v-for="attachment in props.email.attachments"
:key="attachment.fileName"
class="d-flex align-center"
>
<VImg
:src="attachment.thumbnail"
:alt="attachment.fileName"
aspect-ratio="1"
max-height="24"
max-width="24"
class="me-2"
/>
<span>{{ attachment.fileName }}</span>
</div>
</VCardText>
</template>
</VCard>
<VCard class="ma-6">
<VCardText>
<h6 class="text-h6 font-weight-regular mb-6">
Reply to Ross Geller
</h6>
<!-- 👉 Tiptap editor -->
<div class="tiptap-editor-wrapper">
<div
v-if="editor"
class="d-flex flex-wrap gap-x-2 mb-6"
>
<VIcon
icon="ri-bold"
:color="editor.isActive('bold') ? 'primary' : ''"
size="20"
@click="editor.chain().focus().toggleBold().run()"
/>
<VIcon
:color="editor.isActive('underline') ? 'primary' : ''"
icon="ri-underline"
size="20"
@click="editor.commands.toggleUnderline()"
/>
<VIcon
:color="editor.isActive('italic') ? 'primary' : ''"
icon="ri-italic"
size="20"
@click="editor.chain().focus().toggleItalic().run()"
/>
<VIcon
:color="editor.isActive('bulletList') ? 'primary' : ''"
icon="ri-list-check"
size="20"
@click="editor.chain().focus().toggleBulletList().run()"
/>
<VIcon
:color="editor.isActive('orderedList') ? 'primary' : ''"
icon="ri-list-ordered-2"
size="20"
@click="editor.chain().focus().toggleOrderedList().run()"
/>
<VIcon
icon="ri-links-line"
size="20"
@click="setLink"
/>
<VIcon
icon="ri-image-line"
size="20"
@click="addImage"
/>
</div>
<EditorContent :editor="editor" />
</div>
<div class="d-flex align-center justify-end mt-6">
<VBtn
color="secondary"
variant="plain"
class="me-4"
>
<VIcon icon="ri-attachment-2" />
<span>Attachments</span>
</VBtn>
<VBtn>
<span>Send</span>
<VIcon icon="ri-send-plane-line" />
</VBtn>
</div>
</VCardText>
</VCard>
</PerfectScrollbar>
</template>
</VNavigationDrawer>
</template>
<style lang="scss">
.email-view {
inline-size: 100% !important;
@media only screen and (min-width: 1280px) {
inline-size: calc(100% - 256px) !important;
}
.v-navigation-drawer__content {
display: flex;
flex-direction: column;
}
.ProseMirror {
padding: 0;
block-size: 100px;
overflow-y: auto;
p {
margin-block-end: 0;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
ul,ol{
padding-inline: 1.125rem;
}
}
.is-active {
border-color: rgba(var(--v-theme-primary), var(--v-border-opacity)) !important;
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
color: rgb(var(--v-theme-primary));
}
.ProseMirror-focused{
outline: none !important;
}
}
.email-view-action-bar {
min-block-size: 54px;
}
.mail-content-container {
background-color: rgb(var(--v-theme-on-background), var(--v-hover-opacity));
.mail-header {
min-block-size: 84px;
}
.v-card {
border: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
}
}
</style>

View File

@@ -0,0 +1,94 @@
export const useEmail = () => {
const route = useRoute('apps-email-filter')
const updateEmails = async (ids, data) => {
await $api('apps/email', {
method: 'POST',
body: JSON.stringify({ ids, data }),
})
}
const updateEmailLabels = async (ids, label) => {
await $api('/apps/email', {
method: 'POST',
body: { ids, label },
})
}
const emailMoveToFolderActions = [
{ action: 'inbox', icon: 'ri-mail-line' },
{ action: 'spam', icon: 'ri-spam-2-line' },
{ action: 'trash', icon: 'ri-delete-bin-line' },
]
const labels = [
{
title: 'personal',
color: 'success',
},
{
title: 'company',
color: 'primary',
},
{
title: 'important',
color: 'warning',
},
{
title: 'private',
color: 'error',
},
]
const resolveLabelColor = label => {
if (label === 'personal')
return 'success'
if (label === 'company')
return 'primary'
if (label === 'important')
return 'warning'
if (label === 'private')
return 'error'
return 'secondary'
}
const shallShowMoveToActionFor = action => {
if (action === 'trash')
return route.params.filter !== 'trashed'
else if (action === 'inbox')
return !(route.params.filter === undefined || route.params.filter === 'sent' || route.params.filter === 'draft')
else if (action === 'spam')
return !(route.params.filter === 'spam' || route.params.filter === 'sent' || route.params.filter === 'draft')
return false
}
const moveSelectedEmailTo = (action, selectedEmails) => {
const dataToUpdate = {}
if (action === 'inbox') {
if (route.params.filter === 'trashed')
dataToUpdate.isDeleted = false
dataToUpdate.folder = 'inbox'
}
else if (action === 'spam') {
if (route.params.filter === 'trashed')
dataToUpdate.isDeleted = false
dataToUpdate.folder = 'spam'
}
else if (action === 'trash') {
dataToUpdate.isDeleted = true
}
updateEmails(selectedEmails, dataToUpdate)
}
return {
labels,
resolveLabelColor,
shallShowMoveToActionFor,
emailMoveToFolderActions,
moveSelectedEmailTo,
updateEmails,
updateEmailLabels,
}
}

View File

@@ -0,0 +1,126 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDrawerOpen',
'submit',
])
const invoiceBalance = ref()
const paymentAmount = ref()
const paymentDate = ref('')
const paymentMethod = ref()
const paymentNote = ref('')
const onSubmit = () => {
emit('update:isDrawerOpen', false)
emit('submit', {
invoiceBalance: invoiceBalance.value,
paymentAmount: paymentAmount.value,
paymentDate: paymentDate.value,
paymentMethod: paymentMethod.value,
paymentNote: paymentNote.value,
})
}
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
</script>
<template>
<VNavigationDrawer
temporary
location="end"
:width="400"
:model-value="props.isDrawerOpen"
class="scrollable-content"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Add Payment"
@cancel="$emit('update:isDrawerOpen', false)"
/>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<VForm @submit.prevent="onSubmit">
<VRow>
<VCol cols="12">
<VTextField
v-model="invoiceBalance"
label="Invoice Balance"
type="number"
placeholder="$99"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="paymentAmount"
label="Payment Amount"
type="number"
placeholder="$99"
/>
</VCol>
<VCol cols="12">
<AppDateTimePicker
v-model="paymentDate"
label="Payment Date"
placeholder="Select Date"
/>
</VCol>
<VCol cols="12">
<VSelect
v-model="paymentMethod"
label="Select Payment Method"
placeholder="Select Payment Method"
:items="['Cash', 'Bank Transfer', 'Debit', 'Credit', 'PayPal']"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="paymentNote"
label="Internal Payment Note"
placeholder="Internal Payment Note"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
class="me-3"
>
Send
</VBtn>
<VBtn
type="reset"
color="secondary"
variant="outlined"
@click="$emit('update:isDrawerOpen', false)"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>

View File

@@ -0,0 +1,334 @@
<script setup>
import InvoiceProductEdit from './InvoiceProductEdit.vue'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
const props = defineProps({
data: {
type: null,
required: true,
},
})
const emit = defineEmits([
'push',
'remove',
])
const invoice = ref(props.data.invoice)
const salesperson = ref(props.data.salesperson)
const thanksNote = ref(props.data.thanksNote)
const note = ref(props.data.note)
// 👉 Clients
const clients = ref([])
// 👉 fetchClients
const fetchClients = async () => {
const { data, error } = await useApi('/apps/invoice/clients')
if (error.value)
console.log(error.value)
else
clients.value = data.value
}
fetchClients()
// 👉 Add item function
const addItem = () => {
emit('push', {
title: 'App Design',
cost: 24,
hours: 1,
description: 'Designed UI kit & app pages.',
})
}
const removeProduct = id => {
emit('remove', id)
}
</script>
<template>
<VCard class="pa-12">
<!-- SECTION Header -->
<div class="d-flex flex-wrap justify-space-between flex-column rounded bg-var-theme-background flex-sm-row gap-6 pa-6 mb-6">
<!-- 👉 Left Content -->
<div>
<div class="d-flex align-center mb-6">
<!-- 👉 Logo -->
<VNodeRenderer
:nodes="themeConfig.app.logo"
class="me-3"
/>
<!-- 👉 Title -->
<h6 class="font-weight-medium text-xl text-uppercase">
{{ themeConfig.app.title }}
</h6>
</div>
<!-- 👉 Address -->
<p class="text-high-emphasis mb-0">
Office 149, 450 South Brand Brooklyn
</p>
<p class="text-high-emphasis mb-0">
San Diego County, CA 91905, USA
</p>
<p class="text-high-emphasis mb-0">
+1 (123) 456 7891, +44 (876) 543 2198
</p>
</div>
<!-- 👉 Right Content -->
<div>
<!-- 👉 Invoice Id -->
<div class="d-flex align-start font-weight-medium justify-sm-end flex-column flex-sm-row text-lg mb-3">
<span
class="text-high-emphasis me-4"
style="inline-size: 5.625rem ;"
>Invoice:</span>
<span>
<VTextField
v-model="invoice.id"
disabled
density="compact"
prefix="#"
style="inline-size: 9.5rem;"
/>
</span>
</div>
<!-- 👉 Issue Date -->
<div class="d-flex align-start justify-sm-end flex-column flex-sm-row mb-3">
<span
class="text-high-emphasis me-4"
style="inline-size: 5.625rem;"
>Date Issued:</span>
<span style="inline-size: 9.5rem;">
<AppDateTimePicker
v-model="invoice.issuedDate"
density="compact"
placeholder="YYYY-MM-DD"
:config="{ position: 'auto right' }"
/>
</span>
</div>
<!-- 👉 Due Date -->
<div class="d-flex align-start justify-sm-end flex-column flex-sm-row mb-0">
<span
class="text-high-emphasis me-4"
style="inline-size: 5.625rem;"
>Due Date:</span>
<span style="min-inline-size: 9.5rem;">
<AppDateTimePicker
v-model="invoice.dueDate"
density="compact"
placeholder="YYYY-MM-DD"
:config="{ position: 'auto right' }"
/>
</span>
</div>
</div>
</div>
<!-- !SECTION -->
<VRow>
<VCol class="text-no-wrap">
<h6 class="text-h6 mb-4">
Invoice To:
</h6>
<VSelect
v-model="invoice.client"
:items="clients"
item-title="name"
item-value="name"
placeholder="Select Client"
return-object
class="mb-4"
style="inline-size: 11.875rem;"
/>
<p class="mb-0">
{{ invoice.client.name }}
</p>
<p class="mb-0">
{{ invoice.client.company }}
</p>
<p
v-if="invoice.client.address"
class="mb-0"
>
{{ invoice.client.address }}, {{ invoice.client.country }}
</p>
<p class="mb-0">
{{ invoice.client.contact }}
</p>
<p class="mb-0">
{{ invoice.client.companyEmail }}
</p>
</VCol>
<VCol class="text-no-wrap">
<h6 class="text-h6 mb-4">
Bill To:
</h6>
<table>
<tbody>
<tr>
<td class="pe-6">
Total Due:
</td>
<td>{{ props.data.paymentDetails.totalDue }}</td>
</tr>
<tr>
<td class="pe-6">
Bank Name:
</td>
<td>{{ props.data.paymentDetails.bankName }}</td>
</tr>
<tr>
<td class="pe-6">
Country:
</td>
<td>{{ props.data.paymentDetails.country }}</td>
</tr>
<tr>
<td class="pe-6">
IBAN:
</td>
<td>
<p class="text-wrap me-4">
{{ props.data.paymentDetails.iban }}
</p>
</td>
</tr>
<tr>
<td class="pe-6">
SWIFT Code:
</td>
<td>{{ props.data.paymentDetails.swiftCode }}</td>
</tr>
</tbody>
</table>
</VCol>
</VRow>
<VDivider class="my-6 border-dashed" />
<!-- 👉 Add purchased products -->
<div class="add-products-form">
<div
v-for="(product, index) in props.data.purchasedProducts"
:key="product.title"
class="mb-4"
>
<InvoiceProductEdit
:id="index"
:data="product"
@remove-product="removeProduct"
/>
</div>
<VBtn
size="small"
prepend-icon="ri-add-line"
@click="addItem"
>
Add Item
</VBtn>
</div>
<VDivider class="my-6 border-dashed" />
<!-- 👉 Total Amount -->
<div class="d-flex justify-space-between flex-wrap flex-column flex-sm-row">
<div class="mb-6 mb-sm-0">
<div class="d-flex align-center mb-4">
<h6 class="text-h6 me-2">
Salesperson:
</h6>
<VTextField
v-model="salesperson"
style="inline-size: 8rem;"
placeholder="John Doe"
/>
</div>
<VTextField
v-model="thanksNote"
placeholder="Thanks for your business"
/>
</div>
<div>
<table class="w-100">
<tbody>
<tr>
<td class="pe-16">
Subtotal:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-h6">
$1800
</h6>
</td>
</tr>
<tr>
<td class="pe-16">
Discount:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-h6">
$28
</h6>
</td>
</tr>
<tr>
<td class="pe-16">
Tax:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-h6">
21%
</h6>
</td>
</tr>
</tbody>
</table>
<VDivider class="mt-4 mb-3" />
<table class="w-100">
<tbody>
<tr>
<td class="pe-16">
Total:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-h6">
$1690
</h6>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<VDivider class="my-6 border-dashed" />
<div>
<h6 class="text-h6 mb-2">
Note:
</h6>
<VTextarea
v-model="note"
placeholder="Write note here..."
:rows="2"
/>
</div>
</VCard>
</template>

View File

@@ -0,0 +1,207 @@
<!-- eslint-disable vue/no-mutating-props -->
<script setup>
const props = defineProps({
id: {
type: Number,
required: true,
},
data: {
type: Object,
required: true,
default: () => ({
title: 'App Design',
cost: 24,
hours: 1,
description: 'Designed UI kit & app pages.',
}),
},
})
const emit = defineEmits([
'removeProduct',
'totalAmount',
])
const itemsOptions = [
{
title: 'App Design',
cost: 24,
hours: 1,
description: 'Designed UI kit & app pages.',
},
{
title: 'App Customization',
cost: 26,
hours: 1,
description: 'Customization & Bug Fixes.',
},
{
title: 'ABC Template',
cost: 28,
hours: 1,
description: 'Vuetify admin template.',
},
{
title: 'App Development',
cost: 32,
hours: 1,
description: 'Native App Development.',
},
]
const selectedItem = ref('App Customization')
const localProductData = ref(structuredClone(toRaw(props.data)))
watch(selectedItem, () => {
const item = itemsOptions.filter(obj => {
return obj.title === selectedItem.value
})
localProductData.value = item[0]
})
const removeProduct = () => {
emit('removeProduct', props.id)
}
const totalPrice = computed(() => Number(localProductData.value.cost) * Number(localProductData.value.hours))
watch(totalPrice, () => {
emit('totalAmount', totalPrice.value)
}, { immediate: true })
</script>
<template>
<!-- eslint-disable vue/no-mutating-props -->
<div class="add-products-header mb-2 d-none d-md-flex mb-4">
<VRow class="me-10">
<VCol
cols="12"
md="6"
>
<h6 class="text-h6">
Item
</h6>
</VCol>
<VCol
cols="12"
md="2"
>
<h6 class="text-h6 ps-2">
Cost
</h6>
</VCol>
<VCol
cols="12"
md="2"
>
<h6 class="text-h6 ps-2">
Hours
</h6>
</VCol>
<VCol
cols="12"
md="2"
>
<h6 class="text-h6">
Price
</h6>
</VCol>
</VRow>
</div>
<VCard
flat
border
class="d-flex flex-sm-row flex-column-reverse"
>
<!-- 👉 Left Form -->
<div class="pa-5 flex-grow-1">
<VRow>
<VCol
cols="12"
md="6"
>
<VSelect
v-model="selectedItem"
:items="itemsOptions"
item-title="title"
item-value="title"
label="Select Item"
placeholder="Select Item"
class="mb-5"
/>
<VTextarea
v-model="localProductData.description"
rows="2"
label="Description"
placeholder="Item description"
/>
</VCol>
<VCol
cols="12"
md="2"
sm="4"
>
<VTextField
v-model="localProductData.cost"
type="number"
label="Cost"
placeholder="100"
/>
<div class="text-high-emphasis mt-4">
<p class="mb-1">
Discount
</p>
<span>0%</span>
<span class="mx-2">
0%
<VTooltip activator="parent">Tax 1</VTooltip>
</span>
<span>
0%
<VTooltip activator="parent">Tax 2</VTooltip>
</span>
</div>
</VCol>
<VCol
cols="12"
md="2"
sm="4"
>
<VTextField
v-model="localProductData.hours"
type="number"
label="Hours"
placeholder="5"
/>
</VCol>
<VCol
cols="12"
md="2"
sm="4"
>
<p class="my-2">
<span class="d-inline d-md-none">Price: </span>
<span class="text-high-emphasis">${{ totalPrice }}</span>
</p>
</VCol>
</VRow>
</div>
<!-- 👉 Item Actions -->
<div
class="d-flex flex-column align-end item-actions"
:class="$vuetify.display.smAndUp ? 'border-s' : 'border-b' "
>
<IconBtn @click="removeProduct">
<VIcon
:size="20"
icon="ri-close-line"
/>
</IconBtn>
</div>
</VCard>
</template>

View File

@@ -0,0 +1,133 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDrawerOpen',
'submit',
])
const emailFrom = ref('shelbyComapny@email.com')
const emailTo = ref('qConsolidated@email.com')
const invoiceSubject = ref('Invoice of purchased Admin Templates')
const paymentMessage = ref(`Dear Queen Consolidated,
Thank you for your business, always a pleasure to work with you!
We have generated a new invoice in the amount of $95.59
We would appreciate payment of this invoice by 05/11/2019`)
const onSubmit = () => {
emit('update:isDrawerOpen', false)
emit('submit', {
emailFrom: emailFrom.value,
emailTo: emailTo.value,
invoiceSubject: invoiceSubject.value,
paymentMessage: paymentMessage.value,
})
}
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
</script>
<template>
<VNavigationDrawer
temporary
location="end"
:width="400"
:model-value="props.isDrawerOpen"
class="scrollable-content"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Send Invoice"
@cancel="$emit('update:isDrawerOpen', false)"
/>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<VForm @submit.prevent="onSubmit">
<VRow>
<VCol cols="12">
<VTextField
v-model="emailFrom"
label="From"
placeholder="sender@email.com"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="emailTo"
label="To"
placeholder="receiver@email.com"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="invoiceSubject"
label="Subject"
placeholder="Invoice of purchased Admin Templates"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="paymentMessage"
rows="10"
label="Message"
placeholder="Thank you for your business, always a pleasure to work with you!"
/>
</VCol>
<VCol cols="12">
<div class="mb-6">
<VChip
label
color="primary"
size="small"
>
<VIcon
start
icon="ri-links-line"
/>
Invoice Attached
</VChip>
</div>
<VBtn
type="submit"
class="me-3"
>
Send
</VBtn>
<VBtn
color="secondary"
variant="outlined"
@click="$emit('update:isDrawerOpen', false)"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,110 @@
<script setup>
const logisticData = ref([
{
icon: 'ri-car-line',
color: 'primary',
title: 'On route vehicles',
value: 42,
change: 18.2,
isHover: false,
},
{
icon: 'ri-alert-line',
color: 'warning',
title: 'Vehicles with errors',
value: 8,
change: -8.7,
isHover: false,
},
{
icon: 'ri-stackshare-line',
color: 'error',
title: 'Deviated from route',
value: 27,
change: 4.3,
isHover: false,
},
{
icon: 'ri-timer-line',
color: 'info',
title: 'Late vehicles',
value: 13,
change: -2.5,
isHover: false,
},
])
</script>
<template>
<VRow>
<VCol
v-for="(data, index) in logisticData"
:key="index"
cols="12"
md="3"
sm="6"
>
<div>
<VCard
class="logistics-card-statistics cursor-pointer"
:style="data.isHover ? `border-block-end-color: rgb(var(--v-theme-${data.color}))` : `border-block-end-color: rgba(var(--v-theme-${data.color}),0.7)`"
@mouseenter="data.isHover = true"
@mouseleave="data.isHover = false"
>
<VCardText>
<div class="d-flex align-center gap-x-4 mb-2">
<VAvatar
variant="tonal"
:color="data.color"
rounded
>
<VIcon
:icon="data.icon"
size="24"
/>
</VAvatar>
<h4 class="text-h4">
{{ data.value }}
</h4>
</div>
<h6 class="text-h6 font-weight-regular">
{{ data.title }}
</h6>
<div class="d-flex align-center">
<div class="text-body-1 font-weight-medium me-2">
{{ data.change }}%
</div>
<span class="text-sm text-disabled">than last week</span>
</div>
</VCardText>
</VCard>
</div>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
@use "@core-scss/base/mixins" as mixins;
.logistics-card-statistics {
border-block-end-style: solid;
border-block-end-width: 2px;
&:hover {
border-block-end-width: 3px;
margin-block-end: -1px;
@include mixins.elevation(10);
transition: all 0.1s ease-out;
}
}
.skin--bordered{
.logistics-card-statistics {
&:hover {
margin-block-end: -2px;
}
}
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup>
const chartColors = {
donut: {
series1: '#56ca00',
series2: '#56ca00cc',
series3: '#56ca0099',
series4: '#56ca0066',
},
}
const headingColor = 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))'
const labelColor = 'rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity))'
const deliveryExceptionsChartSeries = [
13,
25,
22,
40,
]
const deliveryExceptionsChartConfig = {
labels: [
'Incorrect address',
'Weather conditions',
'Federal Holidays',
'Damage during transit',
],
colors: [
chartColors.donut.series1,
chartColors.donut.series2,
chartColors.donut.series3,
chartColors.donut.series4,
],
stroke: { width: 0 },
dataLabels: {
enabled: false,
formatter(val) {
return `${ Number.parseInt(val) }%`
},
},
legend: {
show: true,
position: 'bottom',
offsetY: 10,
markers: {
width: 8,
height: 8,
offsetX: -3,
},
itemMargin: {
horizontal: 15,
vertical: 5,
},
fontSize: '13px',
fontWeight: 400,
labels: {
colors: headingColor,
useSeriesColors: false,
},
},
tooltip: { theme: false },
grid: { padding: { top: 15 } },
plotOptions: {
pie: {
donut: {
size: '75%',
labels: {
show: true,
value: {
fontSize: '26px',
color: headingColor,
fontWeight: 500,
offsetY: -15,
formatter(val) {
return `${ Number.parseInt(val) }%`
},
},
name: { offsetY: 30 },
total: {
show: true,
fontSize: '1rem',
label: 'AVG. Exceptions',
color: labelColor,
formatter() {
return '30%'
},
},
},
},
},
},
responsive: [{
breakpoint: 420,
options: { chart: { height: 400 } },
}],
}
</script>
<template>
<VCard>
<VCardItem title="Delivery exceptions">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<VueApexCharts
type="donut"
height="400"
:options="deliveryExceptionsChartConfig"
:series="deliveryExceptionsChartSeries"
/>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,101 @@
<script setup>
const deliveryData = [
{
title: 'Packages in transit',
value: '10k',
change: 25.8,
icon: 'ri-gift-line',
color: 'primary',
},
{
title: 'Packages out for delivery',
value: '5k',
change: 4.3,
icon: 'ri-car-line',
color: 'info',
},
{
title: 'Packages delivered',
value: '15k',
change: -12.5,
icon: 'ri-check-line',
color: 'success',
},
{
title: 'Delivery success rate',
value: '95%',
change: 35.6,
icon: 'ri-home-line',
color: 'warning',
},
{
title: 'Average delivery time',
value: '2.5 Days',
change: -2.15,
icon: 'ri-timer-line',
color: 'secondary',
},
{
title: 'Customer satisfaction',
value: '4.5/5',
change: 5.7,
icon: 'ri-user-line',
color: 'error',
},
]
</script>
<template>
<VCard>
<VCardItem
title="Delivery performance"
subtitle="12% increase in this month"
>
<template #append>
<MoreBtn class="mt-n5" />
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="(data, index) in deliveryData"
:key="index"
>
<template #prepend>
<VAvatar
:color="data.color"
variant="tonal"
rounded
size="42"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</template>
<VListItemTitle>{{ data.title }}</VListItemTitle>
<VListItemSubtitle>
<div
:class="data.change > 0 ? 'text-success' : 'text-error'"
class="d-flex align-center"
>
<VIcon
:icon="data.change > 0 ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"
size="24"
class="me-1"
/>
<span>{{ data.change }}%</span>
</div>
</VListItemSubtitle>
<template #append>
<span class="text-high-emphasis text-body-1 font-weight-medium">
{{ data.value }}
</span>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,347 @@
<script setup>
const currentTab = ref('New')
const tabsData = [
'New',
'Preparing',
'Shipping',
]
</script>
<template>
<VCard>
<VCardItem
title="Orders by countries"
subtitle="62 deliveries in progress"
>
<template #append>
<MoreBtn class="mt-n5" />
</template>
</VCardItem>
<VTabs
v-model="currentTab"
grow
class="disable-tab-transition"
>
<VTab
v-for="(tab, index) in tabsData"
:key="index"
>
{{ tab }}
</VTab>
</VTabs>
<VCardText>
<VWindow v-model="currentTab">
<VWindowItem>
<div>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="text-body-2 mb-1">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="text-body-2">
939 Orange, California(CA), 92118
</div>
</VTimelineItem>
</VTimeline>
<VDivider
class="my-4"
style="border-style: dashed;"
/>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="text-body-2 mb-1">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="text-body-2">
487 Sunset, California(CA), 94043
</div>
</VTimelineItem>
</VTimeline>
</div>
</VWindowItem>
<VWindowItem>
<div>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="text-body-2">
939 Orange, California(CA), 92118
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="text-body-2">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
</VTimeline>
<VDivider
class="my-4"
style="border-style: dashed;"
/>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="text-body-2">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="text-body-2">
487 Sunset, California(CA), 94043
</div>
</VTimelineItem>
</VTimeline>
</div>
</VWindowItem>
<VWindowItem>
<div>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="text-body-2">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="text-body-2">
939 Orange, California(CA), 92118
</div>
</VTimelineItem>
</VTimeline>
<VDivider
class="my-4"
style="border-style: dashed;"
/>
<VTimeline
align="start"
truncate-line="both"
side="end"
density="compact"
line-thickness="1"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="text-body-2">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="text-body-2">
487 Sunset, California(CA), 94043
</div>
</VTimelineItem>
</VTimeline>
</div>
</VWindowItem>
</VWindow>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,187 @@
<script setup>
const itemsPerPage = ref(5)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const { data: vehiclesData } = await useApi(createUrl('/apps/logistics/vehicles', {
query: {
page,
itemsPerPage,
sortBy,
orderBy,
},
}))
const vehicles = computed(() => vehiclesData.value.vehicles)
const totalVehicles = computed(() => vehiclesData.value.totalVehicles)
const headers = [
{
title: 'LOCATION',
key: 'location',
},
{
title: 'STARTING ROUTE',
key: 'startRoute',
},
{
title: 'ENDING ROUTE',
key: 'endRoute',
},
{
title: 'WARNINGS',
key: 'warnings',
},
{
title: 'PROGRESS',
key: 'progress',
},
]
const resolveChipColor = warning => {
if (warning === 'No Warnings')
return 'success'
if (warning === 'fuel problems')
return 'primary'
if (warning === 'Temperature Not Optimal')
return 'warning'
if (warning === 'Ecu Not Responding')
return 'error'
if (warning === 'Oil Leakage')
return 'info'
}
</script>
<template>
<VCard>
<VCardItem title="On Route vehicles">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
:items-per-page-options="[
{ value: 5, title: '5' },
{ value: 10, title: '10' },
{ value: 20, title: '20' },
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
]"
:items-length="totalVehicles"
:items="vehicles"
item-value="location"
:headers="headers"
show-select
class="text-no-wrap"
@update:options="updateOptions"
>
<template #item.location="{ item }">
<div class="py-2">
<VAvatar
variant="tonal"
class="me-4"
color="secondary"
>
<VIcon
icon="ri-bus-line"
size="28"
/>
</VAvatar>
<RouterLink
:to="{ name: 'apps-logistics-fleet' }"
class="text-base text-high-emphasis"
>
VOL-{{ item.location }}
</RouterLink>
</div>
</template>
<template #item.startRoute="{ item }">
{{ item.startCity }}, {{ item.startCountry }}
</template>
<template #item.endRoute="{ item }">
{{ item.endCity }}, {{ item.endCountry }}
</template>
<template #item.warnings="{ item }">
<VChip
:color="resolveChipColor(item.warnings)"
size="small"
>
{{ item.warnings }}
</VChip>
</template>
<template #item.progress="{ item }">
<div
class="d-flex align-center gap-x-4"
style="min-inline-size: 240px;"
>
<div class="w-100">
<VProgressLinear
:model-value="item.progress"
rounded
color="primary"
:height="8"
/>
</div>
<div>
{{ item.progress }}%
</div>
</div>
</template>
<!-- Pagination -->
<template #bottom>
<VDivider />
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
Rows Per Page:
<VSelect
v-model="itemsPerPage"
class="per-page-select"
variant="plain"
:items="[10, 20, 25, 50, 100]"
/>
</div>
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalVehicles) }}
</p>
<div class="d-flex gap-x-2 align-center me-2">
<VBtn
class="flip-in-rtl"
icon="ri-arrow-left-s-line"
variant="text"
density="comfortable"
color="high-emphasis"
:disabled="page <= 1"
@click="page <= 1 ? page = 1 : page--"
/>
<VBtn
class="flip-in-rtl"
icon="ri-arrow-right-s-line"
density="comfortable"
variant="text"
color="high-emphasis"
:disabled="page >= Math.ceil(totalVehicles / itemsPerPage)"
@click="page >= Math.ceil(totalVehicles / itemsPerPage) ? page = Math.ceil(totalVehicles / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,256 @@
<script setup>
const chartColors = {
line: {
series1: '#FFB400',
series2: '#9055FD',
},
}
const headingColor = 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))'
const labelColor = 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))'
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
const series = [
{
name: 'Shipment',
type: 'column',
data: [
38,
45,
33,
38,
32,
50,
48,
40,
42,
37,
],
},
{
name: 'Delivery',
type: 'line',
data: [
23,
28,
23,
32,
28,
44,
32,
38,
26,
34,
],
},
]
const shipmentConfig = {
chart: {
type: 'line',
stacked: false,
parentHeightOffset: 0,
toolbar: { show: false },
zoom: { enabled: false },
},
markers: {
size: 5,
colors: '#fff',
strokeColors: chartColors.line.series2,
hover: { size: 6 },
borderRadius: 4,
},
stroke: {
curve: 'smooth',
width: [
0,
3,
],
lineCap: 'round',
},
legend: {
show: true,
position: 'bottom',
markers: {
width: 8,
height: 8,
offsetX: -3,
},
height: 40,
offsetY: 10,
itemMargin: {
horizontal: 10,
vertical: 0,
},
fontSize: '15px',
fontFamily: 'Inter',
fontWeight: 400,
labels: {
colors: headingColor,
useSeriesColors: !1,
},
},
grid: {
strokeDashArray: 8,
borderColor,
xaxis: { lines: { show: false } },
},
colors: [
chartColors.line.series1,
chartColors.line.series2,
],
fill: {
opacity: [
1,
1,
],
},
plotOptions: {
bar: {
columnWidth: '30%',
borderRadius: 4,
borderRadiusApplication: 'around',
},
},
dataLabels: { enabled: false },
xaxis: {
tickAmount: 10,
categories: [
'1 Jan',
'2 Jan',
'3 Jan',
'4 Jan',
'5 Jan',
'6 Jan',
'7 Jan',
'8 Jan',
'9 Jan',
'10 Jan',
],
labels: {
style: {
colors: labelColor,
fontSize: '13px',
fontFamily: 'Inter',
fontWeight: 400,
},
},
axisBorder: {
show: false,
offsetY: 8,
},
axisTicks: { show: false },
},
yaxis: {
tickAmount: 4,
min: 10,
max: 50,
labels: {
style: {
colors: labelColor,
fontSize: '13px',
fontFamily: 'Inter',
fontWeight: 400,
},
formatter(val) {
return `${ val }%`
},
},
},
responsive: [
{
breakpoint: 1400,
options: {
chart: { height: 310 },
xaxis: { labels: { style: { fontSize: '10px' } } },
legend: {
itemMargin: {
vertical: 0,
horizontal: 10,
},
fontSize: '13px',
offsetY: 12,
},
},
},
{
breakpoint: 1025,
options: {
chart: { height: 415 },
plotOptions: { bar: { columnWidth: '50%' } },
},
},
{
breakpoint: 982,
options: { plotOptions: { bar: { columnWidth: '30%' } } },
},
{
breakpoint: 480,
options: {
chart: { height: 250 },
legend: { offsetY: 7 },
},
},
],
}
</script>
<template>
<VCard>
<VCardItem
title="Shipment statistics"
subtitle="Total number of deliveries 23.8k"
>
<template #append>
<VBtnGroup
density="compact"
variant="outlined"
divided
>
<VBtn color="primary">
January
</VBtn>
<VBtn
icon="ri-arrow-down-s-line"
color="primary"
/>
</VBtnGroup>
</template>
</VCardItem>
<VCardText>
<VueApexCharts
id="shipment-statistics"
height="320"
:options="shipmentConfig"
:series="series"
/>
</VCardText>
</VCard>
</template>
<style lang="scss">
@use "@core-scss/template/libs/apex-chart.scss";
.v-btn-group--divided .v-btn:not(:last-child) {
border-inline-end-color: rgba(var(--v-theme-primary), 0.5);
}
#shipment-statistics {
.apexcharts-legend-text {
font-size: 15px !important;
line-height: 22px;
}
.apexcharts-legend{
.apexcharts-legend-series {
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
border-radius: 0.375rem;
block-size: 83%;
padding-block: 4px;
padding-inline: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,154 @@
<script setup>
const vehicleData = [
{
icon: 'ri-car-line',
title: 'On the way',
time: '2hr 10min',
percentage: 39.7,
color: {
bg: 'rgba(var(--v-theme-on-surface), var(--v-hover-opacity))',
text: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
},
},
{
icon: 'ri-download-cloud-2-line',
title: 'Unloading',
time: '3hr 15min',
percentage: 28.3,
color: {
bg: 'rgb(var(--v-theme-primary))',
text: 'rgb(var(--v-theme-on-primary))',
},
},
{
icon: 'ri-upload-line',
title: 'Loading',
time: '1hr 24min',
percentage: 17.4,
color: {
bg: 'rgb(var(--v-theme-info))',
text: 'rgb(var(--v-theme-on-primary))',
},
},
{
icon: 'ri-timer-line',
title: 'Waiting',
time: '5hr 19min',
percentage: 14.6,
color: {
bg: 'rgb(var(--v-tooltip-background))',
text: 'rgba(var(--v-theme-surface))',
},
},
]
</script>
<template>
<VCard>
<VCardItem title="Vehicles Overview">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<div class="d-flex mb-6">
<div
v-for="(item, index) in vehicleData"
:key="item.title"
:style="`inline-size: ${item.percentage}%;`"
>
<div class="vehicle-progress-label position-relative mb-4 text-body-1 d-none d-sm-block">
{{ item.title }}
</div>
<VProgressLinear
:color="item.color.bg"
model-value="100"
height="46"
:class="index === 0 ? 'rounded-s' : index === vehicleData.length - 1 ? 'rounded-e' : ''"
>
<p
class="text-sm font-weight-medium mb-0"
:style="`color: ${item.color.text}`"
>
{{ item.percentage }}%
</p>
</VProgressLinear>
</div>
</div>
<VTable class="text-no-wrap">
<tbody>
<tr
v-for="(vehicle, index) in vehicleData"
:key="index"
>
<td
width="70%"
class="ps-0"
>
<div class="d-flex align-center text-high-emphasis">
<VIcon
:icon="vehicle.icon"
size="24"
class="me-2"
/>
<h6 class="text-h6 font-weight-regular">
{{ vehicle.title }}
</h6>
</div>
</td>
<td>
<h6 class="text-h6">
{{ vehicle.time }}
</h6>
</td>
<td>
<span class="text-body-1">{{ vehicle.percentage }}%</span>
</td>
</tr>
</tbody>
</VTable>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.vehicle-progress-label {
padding-block-end: 1rem;
&::after {
position: absolute;
display: inline-block;
background-color: rgba(var(--v-theme-on-surface), var(--v-border-opacity));
block-size: 10px;
content: "";
inline-size: 2px;
inset-block-end: 0;
inset-inline-start: 0;
[dir="rtl"] & {
inset-inline: unset 0;
}
}
}
</style>
<style lang="scss">
.v-progress-linear__content {
justify-content: start;
padding-inline-start: 1rem;
}
@media (max-width: 1080px) {
.v-progress-linear__content {
padding-inline-start: 0.75rem !important;
}
}
@media (max-width: 576px) {
.v-progress-linear__content {
padding-inline-start: 0.3rem !important;
}
}
</style>

View File

@@ -0,0 +1,326 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import avatar10 from '@images/avatars/avatar-10.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar4 from '@images/avatars/avatar-4.png'
import avatar5 from '@images/avatars/avatar-5.png'
import avatar6 from '@images/avatars/avatar-6.png'
import avatar7 from '@images/avatars/avatar-7.png'
import avatar8 from '@images/avatars/avatar-8.png'
import avatar9 from '@images/avatars/avatar-9.png'
import poseM from '@images/pages/pose_m1.png'
// 👉 Roles List
const roles = ref([
{
role: 'Administrator',
users: [
avatar1,
avatar2,
avatar3,
avatar4,
],
details: {
name: 'Administrator',
permissions: [
{
name: 'User Management',
read: true,
write: true,
create: true,
},
{
name: 'Disputes Management',
read: true,
write: true,
create: true,
},
{
name: 'API Control',
read: true,
write: true,
create: true,
},
],
},
},
{
role: 'Manager',
users: [
avatar1,
avatar2,
avatar3,
avatar4,
avatar5,
avatar6,
avatar7,
],
details: {
name: 'Manager',
permissions: [
{
name: 'Reporting',
read: true,
write: true,
create: false,
},
{
name: 'Payroll',
read: true,
write: true,
create: true,
},
{
name: 'User Management',
read: true,
write: true,
create: true,
},
],
},
},
{
role: 'Users',
users: [
avatar1,
avatar2,
avatar3,
avatar4,
avatar5,
],
details: {
name: 'Users',
permissions: [
{
name: 'User Management',
read: true,
write: false,
create: false,
},
{
name: 'Content Management',
read: true,
write: false,
create: false,
},
{
name: 'Disputes Management',
read: true,
write: false,
create: false,
},
{
name: 'Database Management',
read: true,
write: false,
create: false,
},
],
},
},
{
role: 'Support',
users: [
avatar1,
avatar2,
avatar3,
avatar4,
avatar5,
avatar6,
],
details: {
name: 'Support',
permissions: [
{
name: 'Repository Management',
read: true,
write: true,
create: false,
},
{
name: 'Content Management',
read: true,
write: true,
create: false,
},
{
name: 'Database Management',
read: true,
write: true,
create: false,
},
],
},
},
{
role: 'Restricted User',
users: [
avatar1,
avatar2,
avatar3,
avatar4,
avatar5,
avatar6,
avatar7,
avatar8,
avatar9,
avatar10,
],
details: {
name: 'Restricted User',
permissions: [
{
name: 'User Management',
read: true,
write: false,
create: false,
},
{
name: 'Content Management',
read: true,
write: false,
create: false,
},
{
name: 'Disputes Management',
read: true,
write: false,
create: false,
},
{
name: 'Database Management',
read: true,
write: false,
create: false,
},
],
},
},
])
const isRoleDialogVisible = ref(false)
const roleDetail = ref()
const isAddRoleDialogVisible = ref(false)
const editPermission = value => {
isRoleDialogVisible.value = true
roleDetail.value = value
}
</script>
<template>
<VRow>
<!-- 👉 Roles -->
<VCol
v-for="item in roles"
:key="item.role"
cols="12"
sm="6"
lg="4"
>
<VCard>
<VCardText class="d-flex align-center">
<span>Total {{ item.users.length }} users</span>
<VSpacer />
<div class="v-avatar-group">
<template
v-for="(user, index) in item.users"
:key="user"
>
<VAvatar
v-if="item.users.length > 4 && item.users.length !== 4 && index < 3"
size="40"
:image="user"
/>
<VAvatar
v-if="item.users.length === 4"
size="40"
:image="user"
/>
</template>
<VAvatar
v-if="item.users.length > 4"
:color="$vuetify.theme.name === 'dark' ? '#3F3B59' : '#F0EFF0'"
>
<span class="text-high-emphasis">
+{{ item.users.length - 3 }}
</span>
</VAvatar>
</div>
</VCardText>
<VCardText>
<h5 class="text-h5 mb-1">
{{ item.role }}
</h5>
<div class="d-flex align-center">
<a
href="javascript:void(0)"
@click="editPermission(item.details)"
>
Edit Role
</a>
<VSpacer />
<IconBtn
color="secondary"
class="mt-n2"
>
<VIcon icon="ri-file-copy-line" />
</IconBtn>
</div>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Add New Role -->
<VCol
cols="12"
sm="6"
lg="4"
>
<VCard
class="h-100"
:ripple="false"
>
<VRow
no-gutters
class="h-100"
>
<VCol
cols="5"
class="d-flex flex-column justify-end align-center mt-5"
>
<img
width="65"
:src="poseM"
>
</VCol>
<VCol cols="7">
<VCardText class="d-flex flex-column align-end justify-end gap-4">
<VBtn
size="small"
@click="isAddRoleDialogVisible = true"
>
Add Role
</VBtn>
<span class="text-end">Add role, if it doesn't exist.</span>
</VCardText>
</VCol>
</VRow>
</VCard>
<AddEditRoleDialog v-model:is-dialog-visible="isAddRoleDialogVisible" />
</VCol>
</VRow>
<AddEditRoleDialog
v-model:is-dialog-visible="isRoleDialogVisible"
v-model:role-permissions="roleDetail"
/>
</template>

View File

@@ -0,0 +1,359 @@
<script setup>
const searchQuery = ref('')
const selectedRole = ref()
const selectedPlan = ref()
const selectedStatus = ref()
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
// Headers
const headers = [
{
title: 'User',
key: 'user',
},
{
title: 'Email',
key: 'email',
},
{
title: 'Role',
key: 'role',
},
{
title: 'Plan',
key: 'plan',
},
{
title: 'Status',
key: 'status',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const {
data: usersData,
execute: fetchUsers,
} = await useApi(createUrl('/apps/users', {
query: {
q: searchQuery,
status: selectedStatus,
plan: selectedPlan,
role: selectedRole,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const users = computed(() => usersData.value.users)
const totalUsers = computed(() => usersData.value.totalUsers)
// 👉 search filters
const roles = [
{
title: 'Admin',
value: 'admin',
},
{
title: 'Author',
value: 'author',
},
{
title: 'Editor',
value: 'editor',
},
{
title: 'Maintainer',
value: 'maintainer',
},
{
title: 'Subscriber',
value: 'subscriber',
},
]
const resolveUserRoleVariant = role => {
const roleLowerCase = role.toLowerCase()
if (roleLowerCase === 'subscriber')
return {
color: 'success',
icon: 'ri-user-line',
}
if (roleLowerCase === 'author')
return {
color: 'error',
icon: 'ri-computer-line',
}
if (roleLowerCase === 'maintainer')
return {
color: 'info',
icon: 'ri-pie-chart-line',
}
if (roleLowerCase === 'editor')
return {
color: 'warning',
icon: 'ri-edit-box-line',
}
if (roleLowerCase === 'admin')
return {
color: 'primary',
icon: 'ri-vip-crown-line',
}
return {
color: 'primary',
icon: 'ri-user-line',
}
}
const resolveUserStatusVariant = stat => {
const statLowerCase = stat.toLowerCase()
if (statLowerCase === 'pending')
return 'warning'
if (statLowerCase === 'active')
return 'success'
if (statLowerCase === 'inactive')
return 'secondary'
return 'primary'
}
const deleteUser = async id => {
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
// refetch User
// TODO: Make this async
fetchUsers()
}
</script>
<template>
<section>
<VCard>
<VCardText class="d-flex flex-wrap gap-4">
<!-- 👉 Export button -->
<VBtn
variant="outlined"
color="secondary"
prepend-icon="ri-share-box-line"
>
Export
</VBtn>
<VSpacer />
<div class="app-user-search-filter d-flex flex-wrap gap-4">
<!-- 👉 Search -->
<div style="inline-size: 15.625rem;">
<VTextField
v-model="searchQuery"
placeholder="Search User"
density="compact"
/>
</div>
<!-- 👉 Add user button -->
<div style="inline-size: 10rem;">
<VSelect
v-model="selectedRole"
placeholder="Select Role"
:items="roles"
density="compact"
clearable
clear-icon="ri-close-line"
/>
</div>
</div>
</VCardText>
<!-- SECTION datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
:items-per-page-options="[
{ value: 10, title: '10' },
{ value: 20, title: '20' },
{ value: 50, title: '50' },
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
]"
:items="users"
item-value="id"
:items-length="totalUsers"
:headers="headers"
show-select
class="text-no-wrap rounded-0"
@update:options="updateOptions"
>
<!-- User -->
<template #item.user="{ item }">
<div class="d-flex">
<VAvatar
size="34"
:variant="!item.avatar ? 'tonal' : undefined"
:color="!item.avatar ? resolveUserRoleVariant(item.role).color : undefined"
class="me-3"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column">
<RouterLink
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
class="text-h6"
>
{{ item.fullName }}
</RouterLink>
<span class="text-sm">@{{ item.username }}</span>
</div>
</div>
</template>
<!-- Role -->
<template #item.role="{ item }">
<div class="d-flex gap-4">
<VIcon
size="22"
:icon="resolveUserRoleVariant(item.role).icon"
:color="resolveUserRoleVariant(item.role).color"
/>
<h6 class="text-h6 text-capitalize font-weight-regular">
{{ item.role }}
</h6>
</div>
</template>
<!-- Plan -->
<template #item.plan="{ item }">
<h6 class="text-h6 font-weight-regular text-capitalize">
{{ item.currentPlan }}
</h6>
</template>
<!-- Status -->
<template #item.status="{ item }">
<VChip
:color="resolveUserStatusVariant(item.status)"
size="small"
class="text-capitalize"
>
{{ item.status }}
</VChip>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn
size="small"
@click="deleteUser(item.id)"
>
<VIcon icon="ri-delete-bin-7-line" />
</IconBtn>
<IconBtn
size="small"
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
>
<VIcon icon="ri-eye-line" />
</IconBtn>
<IconBtn size="small">
<VIcon icon="ri-more-2-line" />
<VMenu activator="parent">
<VList>
<VListItem link>
<template #prepend>
<VIcon
size="20"
icon="ri-edit-box-line"
/>
</template>
<VListItemTitle>Edit</VListItemTitle>
</VListItem>
<VListItem link>
<template #prepend>
<VIcon
size="20"
icon="ri-download-line"
/>
</template>
<VListItemTitle>Download</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
<!-- Pagination -->
<template #bottom>
<VDivider />
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
Rows Per Page:
<VSelect
v-model="itemsPerPage"
class="per-page-select"
variant="plain"
:items="[10, 20, 25, 50, 100]"
/>
</div>
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalUsers) }}
</p>
<div class="d-flex gap-x-2 align-center me-2">
<VBtn
class="flip-in-rtl"
icon="ri-arrow-left-s-line"
variant="text"
density="comfortable"
color="high-emphasis"
:disabled="page <= 1"
@click="page <= 1 ? page = 1 : page--"
/>
<VBtn
class="flip-in-rtl"
icon="ri-arrow-right-s-line"
density="comfortable"
variant="text"
color="high-emphasis"
:disabled="page >= Math.ceil(totalUsers / itemsPerPage)"
@click="page >= Math.ceil(totalUsers / itemsPerPage) ? page = Math.ceil(totalUsers / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
<!-- SECTION -->
</VCard>
</section>
</template>
<style lang="scss">
.text-capitalize {
text-transform: capitalize;
}
</style>

View File

@@ -0,0 +1,212 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDrawerOpen',
'userData',
])
const isFormValid = ref(false)
const refForm = ref()
const fullName = ref('')
const userName = ref('')
const email = ref('')
const company = ref('')
const country = ref()
const contact = ref('')
const role = ref()
const plan = ref()
const status = ref()
// 👉 drawer close
const closeNavigationDrawer = () => {
emit('update:isDrawerOpen', false)
nextTick(() => {
refForm.value?.reset()
refForm.value?.resetValidation()
})
}
const onSubmit = () => {
refForm.value?.validate().then(({ valid }) => {
if (valid) {
emit('userData', {
id: 0,
fullName: fullName.value,
company: company.value,
role: role.value,
username: userName.value,
country: country.value,
contact: contact.value,
email: email.value,
currentPlan: plan.value,
status: status.value,
avatar: '',
})
emit('update:isDrawerOpen', false)
nextTick(() => {
refForm.value?.reset()
refForm.value?.resetValidation()
})
}
})
}
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
</script>
<template>
<VNavigationDrawer
temporary
:width="400"
location="end"
class="scrollable-content"
:model-value="props.isDrawerOpen"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Title -->
<AppDrawerHeaderSection
title="Add User"
@cancel="closeNavigationDrawer"
/>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<!-- 👉 Form -->
<VForm
ref="refForm"
v-model="isFormValid"
@submit.prevent="onSubmit"
>
<VRow>
<!-- 👉 Full name -->
<VCol cols="12">
<VTextField
v-model="fullName"
:rules="[requiredValidator]"
label="Full Name"
placeholder="John Doe"
/>
</VCol>
<!-- 👉 Username -->
<VCol cols="12">
<VTextField
v-model="userName"
:rules="[requiredValidator]"
label="Username"
placeholder="Johndoe"
/>
</VCol>
<!-- 👉 Email -->
<VCol cols="12">
<VTextField
v-model="email"
:rules="[requiredValidator, emailValidator]"
label="Email"
placeholder="johndoe@email.com"
/>
</VCol>
<!-- 👉 company -->
<VCol cols="12">
<VTextField
v-model="company"
:rules="[requiredValidator]"
label="Company"
placeholder="Themeselection"
/>
</VCol>
<!-- 👉 Country -->
<VCol cols="12">
<VSelect
v-model="country"
label="Select Country"
placeholder="Select Country"
:rules="[requiredValidator]"
:items="['USA', 'UK', 'India', 'Australia']"
/>
</VCol>
<!-- 👉 Contact -->
<VCol cols="12">
<VTextField
v-model="contact"
type="number"
:rules="[requiredValidator]"
label="Contact"
placeholder="+1-541-754-3010"
/>
</VCol>
<!-- 👉 Role -->
<VCol cols="12">
<VSelect
v-model="role"
label="Select Role"
placeholder="Select Role"
:rules="[requiredValidator]"
:items="['Admin', 'Author', 'Editor', 'Maintainer', 'Subscriber']"
/>
</VCol>
<!-- 👉 Plan -->
<VCol cols="12">
<VSelect
v-model="plan"
label="Select Plan"
placeholder="Select Plan"
:rules="[requiredValidator]"
:items="['Basic', 'Company', 'Enterprise', 'Team']"
/>
</VCol>
<!-- 👉 Status -->
<VCol cols="12">
<VSelect
v-model="status"
label="Select Status"
placeholder="Select Status"
:rules="[requiredValidator]"
:items="[{ title: 'Active', value: 'active' }, { title: 'Inactive', value: 'inactive' }, { title: 'Pending', value: 'pending' }]"
/>
</VCol>
<!-- 👉 Submit and Cancel -->
<VCol cols="12">
<VBtn
type="submit"
class="me-4"
>
Submit
</VBtn>
<VBtn
type="reset"
variant="outlined"
color="error"
@click="closeNavigationDrawer"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,373 @@
<script setup>
const props = defineProps({
userData: {
type: Object,
required: true,
},
})
const standardPlan = {
plan: 'Standard',
price: 99,
benefits: [
'10 Users',
'Up to 10GB storage',
'Basic Support',
],
}
const isUserInfoEditDialogVisible = ref(false)
const isUpgradePlanDialogVisible = ref(false)
const resolveUserStatusVariant = stat => {
if (stat === 'pending')
return 'warning'
if (stat === 'active')
return 'success'
if (stat === 'inactive')
return 'secondary'
return 'primary'
}
const resolveUserRoleVariant = role => {
if (role === 'subscriber')
return {
color: 'primary',
icon: 'ri-user-line',
}
if (role === 'author')
return {
color: 'warning',
icon: 'ri-settings-2-line',
}
if (role === 'maintainer')
return {
color: 'success',
icon: 'ri-database-2-line',
}
if (role === 'editor')
return {
color: 'info',
icon: 'ri-pencil-line',
}
if (role === 'admin')
return {
color: 'error',
icon: 'ri-server-line',
}
return {
color: 'primary',
icon: 'ri-user-line',
}
}
</script>
<template>
<VRow>
<!-- SECTION User Details -->
<VCol cols="12">
<VCard v-if="props.userData">
<VCardText class="text-center pt-12 pb-6">
<!-- 👉 Avatar -->
<VAvatar
rounded
:size="120"
:color="!props.userData.avatar ? 'primary' : undefined"
:variant="!props.userData.avatar ? 'tonal' : undefined"
>
<VImg
v-if="props.userData.avatar"
:src="props.userData.avatar"
/>
<span
v-else
class="text-5xl font-weight-medium"
>
{{ avatarText(props.userData.fullName) }}
</span>
</VAvatar>
<!-- 👉 User fullName -->
<h5 class="text-h5 mt-4">
{{ props.userData.fullName }}
</h5>
<!-- 👉 Role chip -->
<VChip
:color="resolveUserRoleVariant(props.userData.role).color"
size="small"
class="text-capitalize mt-4"
>
{{ props.userData.role }}
</VChip>
</VCardText>
<VCardText class="d-flex justify-center flex-wrap gap-6 pb-6">
<!-- 👉 Done task -->
<div class="d-flex align-center me-8">
<VAvatar
:size="40"
rounded
color="primary"
variant="tonal"
class="me-4"
>
<VIcon
size="24"
icon="ri-check-line"
/>
</VAvatar>
<div>
<h6 class="text-h5">
{{ kFormatter(props.userData.taskDone) }}
</h6>
<span>Task Done</span>
</div>
</div>
<!-- 👉 Done Project -->
<div class="d-flex align-center me-4">
<VAvatar
:size="44"
rounded
color="primary"
variant="tonal"
class="me-3"
>
<VIcon
size="24"
icon="ri-briefcase-4-line"
/>
</VAvatar>
<div>
<h6 class="text-h6">
{{ kFormatter(props.userData.projectDone) }}
</h6>
<span>Project Done</span>
</div>
</div>
</VCardText>
<!-- 👉 Details -->
<VCardText class="pb-6">
<h5 class="text-h5">
Details
</h5>
<VDivider class="my-4" />
<!-- 👉 User Details list -->
<VList class="card-list">
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">Username:</span>
<span class="text-body-1">
@{{ props.userData.username }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Billing Email:
</span>
<span class="text-body-1">{{ props.userData.email }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Status:
</span>
<VChip
size="small"
:color="resolveUserStatusVariant(props.userData.status)"
class="text-capitalize"
>
{{ props.userData.status }}
</VChip>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">Role: </span>
<span class="text-capitalize text-body-1">{{ props.userData.role }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Tax ID:
</span>
<span class="text-body-1">
{{ props.userData.taxId }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Contact:
</span>
<span class="text-body-1">{{ props.userData.contact }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Language:
</span>
<span class="text-body-1">{{ props.userData.language }}</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle class="text-sm">
<span class="font-weight-medium">
Country:
</span>
<span class="text-body-1">{{ props.userData.country }}</span>
</VListItemTitle>
</VListItem>
</VList>
</VCardText>
<!-- 👉 Edit and Suspend button -->
<VCardText class="d-flex justify-center">
<VBtn
variant="elevated"
class="me-4"
@click="isUserInfoEditDialogVisible = true"
>
Edit
</VBtn>
<VBtn
variant="outlined"
color="error"
>
Suspend
</VBtn>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
<!-- SECTION Current Plan -->
<VCol cols="12">
<VCard
flat
class="current-plan"
>
<VCardText class="d-flex">
<!-- 👉 Standard Chip -->
<VChip
color="primary"
size="small"
>
Standard
</VChip>
<VSpacer />
<!-- 👉 Current Price -->
<div class="d-flex align-center">
<sup class="text-primary text-lg font-weight-medium">$</sup>
<h1 class="text-h1 text-primary">
99
</h1>
<sub class="mt-3"><h6 class="text-h6 font-weight-regular">month</h6></sub>
</div>
</VCardText>
<VCardText>
<!-- 👉 Price Benefits -->
<VList class="card-list">
<VListItem
v-for="benefit in standardPlan.benefits"
:key="benefit"
>
<div class="d-flex align-center">
<VIcon
size="10"
color="medium-emphasis"
class="me-2"
icon="ri-circle-fill"
/>
<div class="text-medium-emphasis">
{{ benefit }}
</div>
</div>
</VListItem>
</VList>
<!-- 👉 Days -->
<div class="my-6">
<div class="d-flex mt-3 mb-2">
<h6 class="text-h6 font-weight-medium">
Days
</h6>
<VSpacer />
<h6 class="text-h6 font-weight-medium">
26 of 30 Days
</h6>
</div>
<!-- 👉 Progress -->
<VProgressLinear
rounded
:model-value="86"
height="8"
color="primary"
/>
<p class="text-sm mt-1">
4 days remaining
</p>
</div>
<!-- 👉 Upgrade Plan -->
<VBtn
block
@click="isUpgradePlanDialogVisible = true"
>
Upgrade Plan
</VBtn>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
</VRow>
<!-- 👉 Edit user info dialog -->
<UserInfoEditDialog
v-model:isDialogVisible="isUserInfoEditDialogVisible"
:user-data="props.userData"
/>
<!-- 👉 Upgrade plan dialog -->
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: .5rem;
}
.current-plan {
border: 2px solid rgb(var(--v-theme-primary));
}
.text-capitalize {
text-transform: capitalize !important;
}
</style>

View File

@@ -0,0 +1,331 @@
<script setup>
const searchQuery = ref('')
const selectedStatus = ref()
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const isLoading = ref(false)
// 👉 headers
const headers = [
{
title: '#',
key: 'id',
},
{
title: 'Trending',
key: 'trending',
sortable: false,
},
{
title: 'Total',
key: 'total',
},
{
title: 'Issued Date',
key: 'date',
width: '150px',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
width: '150px',
},
]
const {
data: invoiceData,
execute: fetchInvoices,
} = await useApi(createUrl('/apps/invoice', {
query: {
q: searchQuery,
status: selectedStatus,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const invoices = computed(() => invoiceData.value?.invoices)
const totalInvoices = computed(() => invoiceData.value?.totalInvoices)
// 👉 Invoice balance variant resolver
const resolveInvoiceBalanceVariant = (balance, total) => {
if (balance === total)
return {
status: 'Unpaid',
chip: { color: 'error' },
}
if (balance === 0)
return {
status: 'Paid',
chip: { color: 'success' },
}
return {
status: balance,
chip: { variant: 'text' },
}
}
const resolveInvoiceStatusVariantAndIcon = status => {
if (status === 'Partial Payment')
return {
variant: 'warning',
icon: 'ri-line-chart-line',
}
if (status === 'Paid')
return {
variant: 'success',
icon: 'ri-check-line',
}
if (status === 'Downloaded')
return {
variant: 'info',
icon: 'ri-arrow-down-line',
}
if (status === 'Draft')
return {
variant: 'secondary',
icon: 'ri-save-line',
}
if (status === 'Sent')
return {
variant: 'primary',
icon: 'ri-mail-line',
}
if (status === 'Past Due')
return {
variant: 'error',
icon: 'ri-error-warning-line',
}
return {
variant: 'secondary',
icon: 'ri-close-line',
}
}
const computedMoreList = computed(() => {
return paramId => [
{
title: 'Download',
value: 'download',
prependIcon: 'ri-download-line',
},
{
title: 'Edit',
value: 'edit',
prependIcon: 'ri-pencil-line',
to: {
name: 'apps-invoice-edit-id',
params: { id: paramId },
},
},
{
title: 'Duplicate',
value: 'duplicate',
prependIcon: 'ri-stack-line',
},
]
})
const deleteInvoice = async id => {
await $api(`/apps/invoice/${ id }`, { method: 'DELETE' })
fetchInvoices()
}
</script>
<template>
<section v-if="invoices">
<VCard
id="invoice-list"
title=" Invoice List"
>
<template #append>
<!-- 👉 Export invoice -->
<VBtn>
Export
<VIcon
end
class="flip-in-rtl"
icon="ri-arrow-right-line"
/>
</VBtn>
</template>
<!-- SECTION Datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:loading="isLoading"
:items-length="totalInvoices"
:headers="headers"
:items="invoices"
item-value="id"
class="text-no-wrap text-sm rounded-0"
@update:options="updateOptions"
>
<!-- Trending Header -->
<template #header.trending>
<VIcon
size="22"
icon="ri-arrow-up-line"
/>
</template>
<!-- id -->
<template #item.id="{ item }">
<RouterLink :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
#{{ item.id }}
</RouterLink>
</template>
<!-- trending -->
<template #item.trending="{ item }">
<VTooltip>
<template #activator="{ props }">
<VAvatar
:size="28"
v-bind="props"
:color="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant"
variant="tonal"
>
<VIcon
:size="16"
:icon="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).icon"
/>
</VAvatar>
</template>
<p class="mb-0">
{{ item.invoiceStatus }}
</p>
<p class="mb-0">
Balance: {{ item.balance }}
</p>
<p class="mb-0">
Due date: {{ item.dueDate }}
</p>
</VTooltip>
</template>
<!-- Total -->
<template #item.total="{ item }">
${{ item.total }}
</template>
<!-- issued Date -->
<template #item.date="{ item }">
{{ item.issuedDate }}
</template>
<!-- Balance -->
<template #item.balance="{ item }">
<VChip
v-if="typeof ((resolveInvoiceBalanceVariant(item.balance, item.total)).status) === 'string'"
:color="resolveInvoiceBalanceVariant(item.balance, item.total).chip.color"
>
{{ (resolveInvoiceBalanceVariant(item.balance, item.total)).status }}
</VChip>
<span
v-else
class="text-sm text-high-emphasis"
>
{{ Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status) > 0 ? `$${(resolveInvoiceBalanceVariant(item.balance, item.total)).status}` : `-$${Math.abs(Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status))}` }}
</span>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn
size="small"
@click="deleteInvoice(item.id)"
>
<VIcon icon="ri-delete-bin-7-line" />
</IconBtn>
<IconBtn
size="small"
:to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }"
>
<VIcon icon="ri-eye-line" />
</IconBtn>
<MoreBtn
size="small"
:menu-list="computedMoreList(item.id)"
item-props
/>
</template>
<!-- Pagination -->
<template #bottom>
<VDivider />
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
Rows Per Page:
<VSelect
v-model="itemsPerPage"
class="per-page-select"
variant="plain"
:items="[10, 20, 25, 50, 100]"
/>
</div>
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalInvoices) }}
</p>
<div class="d-flex gap-x-2 align-center me-2">
<VBtn
class="flip-in-rtl"
icon="ri-arrow-left-s-line"
variant="text"
density="comfortable"
color="high-emphasis"
:disabled="page <= 1"
@click="page <= 1 ? page = 1 : page--"
/>
<VBtn
class="flip-in-rtl"
icon="ri-arrow-right-s-line"
density="comfortable"
variant="text"
color="high-emphasis"
:disabled="page >= Math.ceil(totalInvoices / itemsPerPage)"
@click="page >= Math.ceil(totalInvoices / itemsPerPage) ? page = Math.ceil(totalInvoices / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
<!-- !SECTION -->
</VCard>
</section>
</template>
<style lang="scss">
#invoice-list {
.invoice-list-actions {
inline-size: 8rem;
}
.invoice-list-search {
inline-size: 12rem;
}
}
</style>

View File

@@ -0,0 +1,403 @@
<script setup>
import americanExpress from '@images/icons/payments/american-express.png'
import mastercard from '@images/icons/payments/mastercard.png'
import visa from '@images/icons/payments/visa.png'
const isUpgradePlanDialogVisible = ref(false)
const currentCardDetails = ref()
const isCardEditDialogVisible = ref(false)
const isCardAddDialogVisible = ref(false)
const isEditAddressDialogVisible = ref(false)
const openEditCardDialog = cardDetails => {
currentCardDetails.value = cardDetails
isCardEditDialogVisible.value = true
}
const creditCards = [
{
name: 'Tom McBride',
number: '4851234567899865',
expiry: '12/24',
isPrimary: true,
type: 'mastercard',
cvv: '123',
image: mastercard,
},
{
name: 'Mildred Wagner',
number: '5531234567895678',
expiry: '02/24',
isPrimary: false,
type: 'visa',
cvv: '456',
image: visa,
},
{
name: 'Lester Jennings',
number: '5531234567890002',
expiry: '08/20',
isPrimary: false,
type: 'visa',
cvv: '456',
image: americanExpress,
},
]
const currentBillingAddress = {
companyName: 'ThemeSelection',
billingEmail: 'gertrude@gmail.com',
taxID: 'TAX-875623',
vatNumber: 'SDF754K77',
address: '100 Water Plant Avenue, Building 1303 Wake Island',
contact: '+1(609) 933-44-22',
country: 'USA',
state: 'Queensland',
zipCode: 403114,
}
const editBillingData = {
firstName: 'Gertrude',
lastName: 'Jennings',
selectedCountry: 'USA',
addressLine1: '100 Water Plant Avenue',
addressLine2: 'Building 1303 Wake Island',
landmark: 'Near Wake Island',
contact: '+1(609) 933-44-22',
country: 'USA',
state: 'Queensland',
zipCode: 403114,
}
</script>
<template>
<VRow>
<!-- 👉 Current Plan -->
<VCol cols="12">
<VCard title="Current Plan">
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<h6 class="text-h6 mb-1">
Your Current Plan is Basic
</h6>
<p>A simple start for everyone</p>
<h6 class="text-h6 mb-1">
Active until Dec 09, 2021
</h6>
<p>We will send you a notification upon Subscription expiration</p>
<h6 class="text-h6 mb-1">
<span class="me-3">$199 Per Month</span>
<VChip
color="primary"
size="small"
>
Popular
</VChip>
</h6>
<p class="mb-0">
Standard plan for small to medium businesses
</p>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 Alert -->
<VAlert
color="warning"
variant="tonal"
icon="ri-alert-line"
closable
>
<VAlertTitle>We need your attention!</VAlertTitle>
<span>Your plan requires update</span>
</VAlert>
<!-- 👉 Progress -->
<div class="d-flex justify-space-between font-weight-bold mt-4 mb-1">
<h6 class="text-h6">
Days
</h6>
<h6 class="text-h6">
26 of 30 Days
</h6>
</div>
<VProgressLinear
rounded
color="primary"
:height="10"
:model-value="75"
/>
<p class="text-sm mt-1">
Your plan requires update
</p>
</VCol>
<VCol cols="12">
<div class="d-flex flex-wrap gap-4">
<VBtn @click="isUpgradePlanDialogVisible = true">
upgrade plan
</VBtn>
<VBtn
color="error"
variant="outlined"
>
Cancel Subscription
</VBtn>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Payment Methods -->
<VCol cols="12">
<VCard title="Payment Methods">
<template #append>
<VBtn
size="small"
prepend-icon="ri-add-line"
@click="isCardAddDialogVisible = !isCardAddDialogVisible"
>
Add Card
</VBtn>
</template>
<VCardText class="d-flex flex-column gap-y-4">
<VCard
v-for="card in creditCards"
:key="card.name"
border
flat
>
<VCardText class="d-flex flex-sm-row flex-column">
<div class="text-no-wrap">
<VImg
:src="card.image"
max-width="90"
width="auto"
:height="25"
/>
<h6 class="text-h6 my-2">
{{ card.name }}
<VChip
v-if="card.isPrimary"
color="primary"
size="small"
>
Primary
</VChip>
</h6>
<span class="text-body-1">**** **** **** {{ card.number.substring(card.number.length - 4) }}</span>
</div>
<VSpacer />
<div class="d-flex flex-column text-sm-end">
<div class="order-sm-0 order-1">
<VBtn
variant="outlined"
class="me-4"
size="small"
@click="openEditCardDialog(card)"
>
Edit
</VBtn>
<VBtn
color="error"
size="small"
variant="outlined"
>
Delete
</VBtn>
</div>
<span class="text-body-2 my-4 order-sm-1 order-0">Card expires at {{ card.expiry }}</span>
</div>
</VCardText>
</VCard>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Billing Address -->
<VCard title="Billing Address">
<template #append>
<VBtn
size="small"
prepend-icon="ri-add-line"
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
>
Edit Address
</VBtn>
</template>
<VCardText>
<VRow>
<VCol
cols="12"
lg="6"
>
<VTable class="billing-address-table">
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Company Name:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.companyName }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Billing Email:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.billingEmail }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Tax ID:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.taxID }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
VAT Number:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.vatNumber }}
</p>
</td>
</tr>
<tr>
<td class="d-flex align-baseline">
<h6 class="text-h6 text-no-wrap">
Billing Address:
</h6>
</td>
<td>
<p class="text-body-1 mb-0">
{{ currentBillingAddress.address }}
</p>
</td>
</tr>
</VTable>
</VCol>
<VCol
cols="12"
lg="6"
>
<VTable class="billing-address-table">
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Contact:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.contact }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Country:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.country }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
State:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentBillingAddress.state }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap">
Zip Code:
</h6>
</td>
<td>
<p class="text-body-1 mb-0">
{{ currentBillingAddress.zipCode }}
</p>
</td>
</tr>
</VTable>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- 👉 Edit Card Dialog -->
<CardAddEditDialog
v-model:isDialogVisible="isCardEditDialogVisible"
:card-details="currentCardDetails"
/>
<!-- 👉 Add Card Dialog -->
<CardAddEditDialog v-model:isDialogVisible="isCardAddDialogVisible" />
<!-- 👉 Edit Address dialog -->
<AddEditAddressDialog
v-model:isDialogVisible="isEditAddressDialogVisible"
:billing-address="editBillingData"
/>
<!-- 👉 Upgrade plan dialog -->
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss">
.billing-address-table {
tr {
td:first-child {
inline-size: 148px;
}
}
}
</style>

View File

@@ -0,0 +1,189 @@
<script setup>
import asana from '@images/icons/brands/asana.png'
import behance from '@images/icons/brands/behance.png'
import dribbble from '@images/icons/brands/dribbble.png'
import facebook from '@images/icons/brands/facebook.png'
import github from '@images/icons/brands/github.png'
import google from '@images/icons/brands/google.png'
import linkedin from '@images/icons/brands/linkedin.png'
import mailchimp from '@images/icons/brands/mailchimp.png'
import slack from '@images/icons/brands/slack.png'
import twitter from '@images/icons/brands/twitter.png'
const connectedAccounts = ref([
{
img: google,
title: 'Google',
text: 'Calendar and contacts',
connected: true,
},
{
img: slack,
title: 'Slack',
text: 'Communication',
connected: false,
},
{
img: github,
title: 'GitHub',
text: 'Manage your Git repositories',
connected: true,
},
{
img: mailchimp,
title: 'Mailchimp',
text: 'Email marketing service',
connected: false,
},
{
img: asana,
title: 'Asana',
text: 'Communication',
connected: false,
},
])
const socialAccounts = ref([
{
img: facebook,
title: 'Facebook',
connected: false,
},
{
img: twitter,
title: 'Twitter',
link: 'https://twitter.com/theme_selection',
username: '@Theme_Selection',
connected: true,
},
{
img: linkedin,
title: 'LinkedIn',
link: 'https://www.linkedin.com/company/themeselection',
username: '@ThemeSelection',
connected: true,
},
{
img: dribbble,
title: 'Dribbble',
connected: false,
},
{
img: behance,
title: 'Behance',
connected: false,
},
])
</script>
<template>
<VRow>
<!-- 👉 connected accounts -->
<VCol cols="12">
<VCard
title="Connected Accounts"
subtitle="Display content from your connected accounts on your site"
>
<VCardText>
<VList class="card-list">
<VListItem
v-for="account in connectedAccounts"
:key="account.title"
>
<template #prepend>
<VAvatar
start
:size="36"
:image="account.img"
/>
</template>
<VListItemTitle class="font-weight-medium">
{{ account.title }}
</VListItemTitle>
<VListItemSubtitle class="text-body-1">
{{ account.text }}
</VListItemSubtitle>
<template #append>
<VSwitch
v-model="account.connected"
density="compact"
class="me-1"
/>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</VCol>
<!-- 👉 social accounts -->
<VCol cols="12">
<VCard
title="Social Accounts"
subtitle="Display content from social accounts on your site"
>
<VCardText>
<VList class="card-list">
<VListItem
v-for="(account) in socialAccounts"
:key="account.title"
>
<template #prepend>
<VAvatar
start
size="36"
rounded="0"
:image="account.img"
/>
</template>
<VListItemTitle class="font-weight-medium">
{{ account.title }}
</VListItemTitle>
<VListItemSubtitle v-if="account.connected">
<a
:href="account.link"
target="_blank"
rel="noopener noreferrer"
class="text-base text-primary"
>
{{ account.username }}
</a>
</VListItemSubtitle>
<VListItemSubtitle
v-else
class="text-body-1"
>
Not connected
</VListItemSubtitle>
<template #append>
<VBtn
icon
:color="account.connected ? 'error' : 'secondary'"
variant="outlined"
class="rounded"
>
<VIcon
size="20"
:icon="account.connected ? 'ri-delete-bin-7-line' : 'ri-link'"
/>
</VBtn>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup>
const notifications = ref([
{
type: 'New for you',
email: true,
browser: false,
app: false,
},
{
type: 'Account activity',
email: false,
browser: true,
app: true,
},
{
type: 'A new browser used to sign in',
email: true,
browser: true,
app: true,
},
{
type: 'A new device is linked',
email: false,
browser: true,
app: false,
},
])
</script>
<template>
<VCard title="Notifications">
<VDivider />
<VCardText>
<h6 class="text-h6">
You will receive notification for the below selected items.
</h6>
</VCardText>
<VTable class="text-no-wrap rounded-0 text-high-emphasis">
<thead>
<tr>
<th scope="col">
TYPE
</th>
<th scope="col">
EMAIL
</th>
<th scope="col">
BROWSER
</th>
<th scope="col">
APP
</th>
</tr>
</thead>
<tbody>
<tr
v-for="notification in notifications"
:key="notification.type"
>
<td>{{ notification.type }}</td>
<td>
<VCheckbox v-model="notification.email" />
</td>
<td>
<VCheckbox v-model="notification.browser" />
</td>
<td>
<VCheckbox v-model="notification.app" />
</td>
</tr>
</tbody>
</VTable>
<VDivider />
<VCardText class="d-flex flex-wrap gap-4">
<VBtn>Save changes</VBtn>
<VBtn
color="secondary"
variant="outlined"
>
Discard
</VBtn>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,286 @@
<script setup>
import { useTheme } from 'vuetify'
import UserInvoiceTable from './UserInvoiceTable.vue'
import pdf from '@images/icons/project-icons/pdf.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar4 from '@images/avatars/avatar-4.png'
import avatar5 from '@images/avatars/avatar-5.png'
import figma from '@images/icons/project-icons/figma.png'
import html5 from '@images/icons/project-icons/html5.png'
import python from '@images/icons/project-icons/python.png'
import react from '@images/icons/project-icons/react.png'
import sketch from '@images/icons/project-icons/sketch.png'
import vue from '@images/icons/project-icons/vue.png'
import xamarin from '@images/icons/project-icons/xamarin.png'
const projectTableHeaders = [
{
title: 'PROJECT',
key: 'name',
},
{
title: 'TOTAL TASK',
key: 'totalTask',
},
{
title: 'PROGRESS',
key: 'progress',
},
{
title: 'HOURS',
key: 'hours',
},
]
const { name } = useTheme()
const projects = [
{
logo: react,
name: 'BGC eCommerce App',
project: 'React Project',
totalTask: '122/240',
progress: 78,
hours: '18:42',
},
{
logo: figma,
name: 'Falcon Logo Design',
project: 'Figma Project',
totalTask: '09/56',
progress: 18,
hours: '20:42',
},
{
logo: vue,
name: 'Dashboard Design',
project: 'Vuejs Project',
totalTask: '290/320',
progress: 62,
hours: '120:87',
},
{
logo: xamarin,
name: 'Foodista mobile app',
project: 'Xamarin Project',
totalTask: '290/320',
progress: 8,
hours: '120:87',
},
{
logo: python,
name: 'Dojo Email App',
project: 'Python Project',
totalTask: '120/186',
progress: 49,
hours: '230:10',
},
{
logo: sketch,
name: 'Blockchain Website',
project: 'Sketch Project',
totalTask: '99/109',
progress: 92,
hours: '342:41',
},
{
logo: html5,
name: 'Hoffman Website',
project: 'HTML Project',
totalTask: '98/110',
progress: 88,
hours: '12:45',
},
]
const resolveUserProgressVariant = progress => {
if (progress <= 25)
return 'error'
if (progress > 25 && progress <= 50)
return 'warning'
if (progress > 50 && progress <= 75)
return 'primary'
if (progress > 75 && progress <= 100)
return 'success'
return 'secondary'
}
const search = ref('')
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="Project List">
<template #append>
<VTextField
v-model="search"
placeholder="Search Project"
density="compact"
style="inline-size: 10rem;"
/>
</template>
<!-- 👉 User Project List Table -->
<!-- SECTION Datatable -->
<VDataTable
:search="search"
:headers="projectTableHeaders"
:items="projects"
item-value="name"
class="text-no-wrap rounded-0"
>
<!-- projects -->
<template #item.name="{ item }">
<div class="d-flex align-center">
<VAvatar
:size="34"
class="me-3"
:image="item.logo"
/>
<div>
<h6 class="text-h6 mb-0">
{{ item.name }}
</h6>
<p class="text-sm text-medium-emphasis mb-0">
{{ item.project }}
</p>
</div>
</div>
</template>
<!-- total task -->
<template #item.totalTask="{ item }">
<div class="text-high-emphasis">
{{ item.totalTask }}
</div>
</template>
<!-- Progress -->
<template #item.progress="{ item }">
<div class="text-high-emphasis">
{{ item.progress }}%
</div>
<VProgressLinear
:height="6"
:model-value="item.progress"
rounded
:color="resolveUserProgressVariant(item.progress)"
/>
</template>
<!-- remove footer -->
<!-- TODO refactor this after vuetify community gives answer -->
<template #bottom />
</VDataTable>
<!-- !SECTION -->
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Activity timeline -->
<VCard title="User Activity Timeline">
<VCardText>
<VTimeline
density="compact"
align="start"
truncate-line="both"
:line-inset="8"
class="v-timeline-density-compact"
>
<VTimelineItem
dot-color="error"
size="x-small"
>
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
<span class="app-timeline-title">
12 Invoices have been paid
</span>
<span class="app-timeline-meta">12 min ago</span>
</div>
<p class="app-timeline-text mb-2">
Invoices have been paid to the company
</p>
<div class="d-inline-flex align-center timeline-chip">
<img
:src="pdf"
height="20"
class="me-2"
alt="img"
>
<span class="app-timeline-text font-weight-medium">
invoice.pdf
</span>
</div>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
<span class="app-timeline-title">
Client Meeting
</span>
<span class="app-timeline-meta">45 min ago</span>
</div>
<p class="app-timeline-text mb-2">
React Project meeting with john @10:15am
</p>
<div class="d-flex align-center mt-3">
<VAvatar
size="32"
class="me-2"
:image="avatar2"
/>
<div>
<p class="text-sm font-weight-medium mb-0">
Lester McCarthy (Client)
</p>
<span class="text-sm">CEO of Kelly Group</span>
</div>
</div>
</VTimelineItem>
<VTimelineItem
dot-color="info"
size="x-small"
>
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
<span class="app-timeline-title">
Create a new project for client
</span>
<span class="app-timeline-meta">2 day ago</span>
</div>
<p class="app-timeline-text mb-2">
6 team members in a project
</p>
<div class="v-avatar-group">
<VAvatar
v-for="avatar in [avatar2, avatar3, avatar4, avatar5]"
:key="avatar"
:image="avatar"
/>
<VAvatar :color="name === 'light' ? '#F0EFF0' : '#3F3B59'">
<span class="text-high-emphasis">+3</span>
</VAvatar>
</div>
</VTimelineItem>
</VTimeline>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<UserInvoiceTable />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,198 @@
<script setup>
import chrome from '@images/logos/chrome.png'
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
const smsVerificationNumber = ref('')
const isTwoFactorDialogOpen = ref(false)
const recentDeviceHeader = [
{
title: 'BROWSER',
key: 'browser',
},
{
title: 'DEVICE',
key: 'device',
},
{
title: 'LOCATION',
key: 'location',
},
{
title: 'RECENT ACTIVITY',
key: 'activity',
},
]
const recentDevices = [
{
browser: 'Chrome on Windows',
logo: chrome,
device: 'Dell XPS 15',
location: 'United States',
activity: '10, Jan 2020 20:07',
},
{
browser: 'Chrome on Android',
logo: chrome,
device: 'Google Pixel 3a',
location: 'Ghana',
activity: '11, Jan 2020 10:16',
},
{
browser: 'Chrome on macOS',
logo: chrome,
device: 'Apple iMac',
location: 'Mayotte',
activity: '11, Jan 2020 12:10',
},
{
browser: 'Chrome on iPhone',
logo: chrome,
device: 'Apple iPhone XR',
location: 'Mauritania',
activity: '12, Jan 2020 8:29',
},
]
</script>
<template>
<VRow>
<VCol cols="12">
<!-- 👉 Change password -->
<VCard title="Change Password">
<VCardText>
<VAlert
variant="tonal"
color="warning"
closable
class="mb-6"
>
<VAlertTitle>Ensure that these requirements are met</VAlertTitle>
<span>Minimum 8 characters long, uppercase & symbol</span>
</VAlert>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
label="New Password"
placeholder="············"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Confirm Password"
placeholder="············"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
<VCol cols="12">
<VBtn type="submit">
Change Password
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Two step verification -->
<VCard
title="Two-step verification"
subtitle="Keep your account secure with authentication step."
>
<VCardText>
<div>
<h4 class="font-weight-medium mb-1">
SMS
</h4>
<VTextField
:model-value="smsVerificationNumber"
readonly
placeholder="+1(968) 819-2547"
density="compact"
>
<template #append>
<IconBtn
rounded
variant="outlined"
color="secondary"
class="me-2"
>
<VIcon
icon="ri-edit-box-line"
@click="isTwoFactorDialogOpen = true"
/>
</IconBtn>
<IconBtn
rounded
variant="outlined"
color="secondary"
>
<VIcon icon="ri-user-add-line" />
</IconBtn>
</template>
</VTextField>
</div>
<p class="mb-0 mt-4">
Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a
href="javascript:void(0)"
class="text-decoration-none"
>Learn more</a>.
</p>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Recent devices -->
<VCard title="Recent devices">
<VDataTable
:items="recentDevices"
:headers="recentDeviceHeader"
hide-default-footer
class="text-no-wrap rounded-0"
>
<template #item.browser="{ item }">
<div class="d-flex align-center">
<VAvatar
:image="item.logo"
:size="22"
class="me-3"
/>
<h6 class="text-h6 font-weight-medium">
{{ item.browser }}
</h6>
</div>
</template>
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
<template #bottom />
</VDataTable>
</VCard>
</VCol>
</VRow>
<!-- 👉 Enable One Time Password Dialog -->
<TwoFactorAuthDialog
v-model:isDialogVisible="isTwoFactorDialogOpen"
:sms-code="smsVerificationNumber"
/>
</template>