Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<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"
>
<span class="text-body-1 text-high-emphasis font-weight-medium">
{{ assignment.progress }}%
</span>
</VProgressCircular>
</template>
<VListItemTitle class="font-weight-medium mb-2 me-2">
{{ assignment.title }}
</VListItemTitle>
<VListItemSubtitle class="me-2">
{{ assignment.tasks }} Tasks
</VListItemSubtitle>
<template #append>
<VBtn
icon
variant="tonal"
color="secondary"
rounded
size="30"
>
<VIcon
icon="tabler-chevron-right"
size="20"
class="flip-in-rtl"
/>
</VBtn>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 2rem;
}
</style>

View File

@@ -0,0 +1,64 @@
<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="d-flex justify-space-between py-4 px-6">
<div class="text-body-1 text-uppercase">
instructors
</div>
<div class="text-body-1 text-uppercase">
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>
<VListItemTitle class="font-weight-medium">
{{ instructor.name }}
</VListItemTitle>
<VListItemSubtitle>
{{ instructor.profession }}
</VListItemSubtitle>
<template #append>
<h6 class="text-h6">
{{ instructor.totalCourses }}
</h6>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss">
.card-list {
--v-card-list-gap: 1rem;
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup>
const coursesData = [
{
title: 'Videography Basic Design Course',
views: '1.2k',
icon: 'tabler-brand-zoom',
color: 'primary',
},
{
title: 'Basic Front-end Development Course',
views: '834',
icon: 'tabler-code',
color: 'info',
},
{
title: 'Basic Fundamentals of Photography',
views: '3.7k',
icon: 'tabler-camera',
color: 'success',
},
{
title: 'Advance Dribble Base Visual Design',
views: '2.5k',
icon: 'tabler-brand-dribbble',
color: 'warning',
},
{
title: 'Your First Singing Lesson',
views: '948',
icon: 'tabler-microphone-2',
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>
<VListItemTitle class="me-4">
<div class="d-flex flex-column">
<h6 class="text-h6 text-truncate">
{{ course.title }}
</h6>
<div>
<VChip
variant="tonal"
color="secondary"
label
size="small"
>
{{ course.views }} Views
</VChip>
</div>
</div>
</VListItemTitle>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,192 @@
<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 => {
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 font-weight-medium">
Courses you are taking
</h5>
<div>
<AppTextField
v-model="searchQuery"
placeholder="Search Course"
style="max-inline-size: 300px;min-inline-size: 300px;"
/>
</div>
</div>
</VCardText>
<VDivider />
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
: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"
:items-length="totalCourse"
show-select
class="text-no-wrap"
@update:options="updateOptions"
>
<!-- Course Name -->
<template #item.courseName="{ item }">
<div class="d-flex align-center gap-x-4 py-2">
<VAvatar
variant="tonal"
size="40"
rounded
:color="item.color"
>
<VIcon
:icon="item.logo"
size="28"
/>
</VAvatar>
<div>
<div class="text-base font-weight-medium mb-1">
<RouterLink
:to="{ name: 'apps-academy-course-details' }"
class="text-link d-inline-block"
>
{{ item.courseTitle }}
</RouterLink>
</div>
<div class="d-flex align-center">
<VAvatar
size="22"
:image="item.image"
/>
<div class="text-body-2 text-high-emphasis ms-2">
{{ item.user }}
</div>
</div>
</div>
</div>
</template>
<template #item.time="{ item }">
<h6 class="text-h6">
{{ item.time }}
</h6>
</template>
<!-- Progress -->
<template #item.progress="{ item }">
<div
class="d-flex align-center gap-x-4"
style="inline-size: 15.625rem;"
>
<div class="text-no-wrap font-weight-medium text-high-emphasis">
{{ 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>
<!-- Status -->
<template #item.status="{ item }">
<div class="d-flex gap-x-5">
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="tabler-users"
color="primary"
size="24"
/>
<div class="text-body-1">
{{ item.userCount }}
</div>
</div>
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="tabler-book"
color="info"
size="24"
/>
<span class="text-body-1">{{ item.note }}</span>
</div>
<div class="d-flex gap-x-2 align-center">
<VIcon
icon="tabler-brand-zoom"
color="error"
size="24"
/>
<span class="text-body-1">{{ item.view }}</span>
</div>
</div>
</template>
<!-- Pagination -->
<template #bottom>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalCourse"
/>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,239 @@
<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 gap-x-6 gap-y-4 align-center">
<AppSelect
v-model="label"
: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: 260px;"
/>
<VSwitch
v-model="hideCompleted"
label="Hide Completed"
/>
</div>
</div>
<!-- 👉 Course List -->
<div
v-if="courses.length"
class="mb-6"
>
<VRow>
<template
v-for="course in courses"
:key="course.id"
>
<VCol
cols="12"
md="4"
sm="6"
>
<VCard
flat
border
>
<div class="px-2 pt-2">
<VImg
:src="course.tutorImg"
class="cursor-pointer"
@click="() => $router.push({ name: 'apps-academy-course-details' })"
/>
</div>
<VCardText>
<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 align-center me-1">
{{ course.rating }}
</h6>
<VIcon
icon="tabler-star-filled"
color="warning"
size="24"
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="tabler-clock"
size="20"
class="me-1"
/>
<span class="text-body-1 my-auto"> {{ course.time }}</span>
</div>
<div
v-else
class="mb-1"
>
<VIcon
icon="tabler-check"
size="20"
color="success"
class="me-1"
/>
<span class="text-success text-body-1">Completed</span>
</div>
<VProgressLinear
:model-value="(course.completedTasks / course.totalTasks) * 100"
rounded
color="primary"
height="8"
class="mb-4"
/>
<div class="d-flex flex-wrap gap-4">
<VBtn
variant="tonal"
color="secondary"
class="flex-grow-1"
:to="{ name: 'apps-academy-course-details' }"
>
<template #prepend>
<VIcon
icon="tabler-rotate-clockwise-2"
class="flip-in-rtl"
/>
</template>
Start Over
</VBtn>
<VBtn
v-if="course.completedTasks !== course.totalTasks"
variant="tonal"
class="flex-grow-1"
:to="{ name: 'apps-academy-course-details' }"
>
<template #append>
<VIcon
icon="tabler-chevron-right"
class="flip-in-rtl"
/>
</template>
Continue
</VBtn>
</div>
</VCardText>
</VCard>
</VCol>
</template>
</VRow>
</div>
<div v-else>
<h4 class="text-h4 text-center mb-6">
No Course Found
</h4>
</div>
<VPagination
v-model="page"
active-color="primary"
first-icon="tabler-chevrons-left"
last-icon="tabler-chevrons-right"
show-first-last-page
: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,203 @@
<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)',
'rgba(var(--v-theme-info),1)',
'rgba(var(--v-theme-success),1)',
'rgba(var(--v-theme-secondary),1)',
'rgba(var(--v-theme-error),1)',
'rgba(var(--v-theme-warning),1)',
],
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',
},
{
title: 'Animation',
value: 12,
color: 'secondary',
},
{
title: 'Vue',
value: 10,
color: 'error',
},
{
title: 'SEO',
value: 9,
color: 'warning',
},
]
</script>
<template>
<VCard>
<VCardItem title="Topic you are interested in">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
xl="8"
lg="7"
>
<div>
<VueApexCharts
type="bar"
height="260"
:options="topicsChartConfig"
:series="topicsChartSeries"
/>
</div>
</VCol>
<VCol
cols="12"
md="6"
lg="5"
xl="4"
>
<div class="topic-progress d-flex flex-wrap gap-x-6 gap-y-10 ms-auto">
<div
v-for="topic in topicsData"
:key="topic.title"
class="d-flex gap-x-2"
>
<VBadge
dot
inline
class="mt-1 custom-badge"
:color="topic.color"
/>
<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>

View File

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

View File

@@ -0,0 +1,341 @@
<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
data-allow-mismatch
temporary
location="end"
:model-value="props.isDrawerOpen"
width="370"
:border="0"
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="tabler-trash"
/>
</IconBtn>
</template>
</AppDrawerHeaderSection>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<!-- SECTION Form -->
<VForm
ref="refForm"
@submit.prevent="handleSubmit"
>
<VRow>
<!-- 👉 Title -->
<VCol cols="12">
<AppTextField
id="event-title"
v-model="event.title"
label="Title"
placeholder="Meeting with Jane"
:rules="[requiredValidator]"
/>
</VCol>
<!-- 👉 Calendar -->
<VCol cols="12">
<AppSelect
id="event-label"
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
:color="item.raw.color"
icon="tabler-circle-filled"
size="8"
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="tabler-circle-filled"
:color="item.raw.color"
/>
</template>
</VListItem>
</template>
</AppSelect>
</VCol>
<!-- 👉 Start date -->
<VCol cols="12">
<AppDateTimePicker
id="event-start-date"
: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
id="event-end-date"
: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
id="event-all-day"
v-model="event.allDay"
label="All day"
/>
</VCol>
<!-- 👉 Event URL -->
<VCol cols="12">
<AppTextField
id="event-url"
v-model="event.url"
label="Event URL"
placeholder="https://event.com/meeting"
:rules="[urlValidator]"
type="url"
/>
</VCol>
<!-- 👉 Guests -->
<VCol cols="12">
<AppSelect
id="event-guests"
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">
<AppTextField
id="event-location"
v-model="event.extendedProps.location"
label="Location"
placeholder="Meeting room"
/>
</VCol>
<!-- 👉 Description -->
<VCol cols="12">
<AppTextarea
id="event-description"
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,302 @@
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) => {
calendarApi.value = refCalendar.value.getApi()
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 => {
// Making API call using $api('', { method: ... })
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(() => {
nextTick(() => {
if (refCalendar.value)
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,179 @@
<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-6 px-6"
:class="$vuetify.locale.isRtl ? 'text-left' : 'text-right'"
>
<IconBtn @click="$emit('close')">
<VIcon
icon="tabler-x"
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-5"
>
<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-capitalize 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-6 px-6"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6">
<div class="text-sm text-disabled">
ABOUT
</div>
<p class="mt-1 mb-6">
{{ store.activeChat.contact.about }}
</p>
</div>
<!-- Personal Information -->
<div class="mb-6">
<div class="text-sm text-disabled mb-1">
PERSONAL INFORMATION
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-mail"
size="22"
/>
<div class="text-base">
lucifer@email.com
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-phone"
size="22"
/>
<div class="text-base">
+1(123) 456 - 7890
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-clock"
size="22"
/>
<div class="text-base">
Mon - Fri 10AM - 8PM
</div>
</div>
</div>
<!-- Options -->
<div>
<div class="text-sm text-disabled mb-1">
OPTIONS
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-badge"
size="22"
/>
<div class="text-base">
Add Tag
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-star"
size="22"
/>
<div class="text-base">
Important Contact
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-photo"
size="22"
/>
<div class="text-base">
Shared Media
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
class="me-2"
icon="tabler-trash"
size="22"
/>
<div class="text-base">
Delete Contact
</div>
</div>
<div class="d-flex align-center text-high-emphasis pa-2">
<VIcon
icon="tabler-ban"
class="me-2"
size="22"
/>
<div class="text-base">
Block Contact
</div>
</div>
<VBtn
block
color="error"
append-icon="tabler-trash"
class="mt-6"
>
Delete Contact
</VBtn>
</div>
</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 }"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="0"
: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 text-high-emphasis mb-0">
{{ props.user.fullName }}
</p>
<p class="mb-0 text-truncate text-body-2">
{{ props.isChatContact && 'chat' in props.user ? props.user.chat.lastMessage.message : props.user.about }}
</p>
</div>
<div
v-if="props.isChatContact && 'chat' in props.user"
class="d-flex flex-column align-self-start"
>
<div class="text-body-2 text-disabled whitespace-no-wrap">
{{ formatDateToMonthShort(props.user.chat.lastMessage.time) }}
</div>
<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 "@core-scss/template/mixins" as templateMixins;
@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: 12px;
@include mixins.before-pseudo;
@include vuetifyStates.states($active: false);
&.chat-contact-active {
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
background: rgb(var(--v-theme-primary));
color: #fff;
--v-theme-on-background: #fff;
}
.v-badge--bordered .v-badge__badge::after {
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<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"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
bordered
>
<VAvatar
size="40"
class="cursor-pointer"
@click="$emit('showUserProfile')"
>
<VImg
:src="store.profileUser.avatar"
alt="John Doe"
/>
</VAvatar>
</VBadge>
<AppTextField
id="search"
v-model="search"
placeholder="Search..."
prepend-inner-icon="tabler-search"
class="ms-4 me-1 chat-list-search"
/>
<IconBtn
v-if="$vuetify.display.smAndDown"
@click="$emit('close')"
>
<VIcon
icon="tabler-x"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<VDivider />
<PerfectScrollbar
tag="ul"
class="d-flex flex-column gap-y-1 chat-contacts-list px-3 py-2 list-none"
:options="{ wheelPropagation: false }"
>
<li class="list-none">
<h5 class="chat-contact-header text-primary text-h5">
Chats
</h5>
</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 pt-2">
<h5 class="chat-contact-header text-primary text-h5">
Contacts
</h5>
</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: 16px;
padding-block-end: 0.75rem;
.chat-contact-header {
margin-block: 0.5rem 0.25rem;
}
.chat-contact-header,
.no-chat-items-text {
margin-inline: var(--chat-content-spacing-x);
}
}
.chat-list-search {
.v-field--focused {
box-shadow: none !important;
}
}
</style>

View File

@@ -0,0 +1,142 @@
<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: 'tabler-checks',
color: 'success',
}
else if (feedback.isDelivered)
return {
icon: 'tabler-checks',
color: undefined,
}
else
return {
icon: 'tabler-check',
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-6">
<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-6': 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 py-2 px-4 elevation-2"
style="background-color: rgb(var(--v-theme-surface));"
:class="[
msgGrp.senderId === contact.id ? 'chat-left' : 'bg-primary text-white chat-right',
msgGrp.messages.length - 1 !== msgIndex ? 'mb-2' : 'mb-1',
]"
>
<p class="mb-0 text-base">
{{ msgData.message }}
</p>
</div>
<div :class="{ 'text-right': msgGrp.senderId !== contact.id }">
<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>
<span class="text-sm ms-2 text-disabled">{{ formatDate(msgGrp.messages[msgGrp.messages.length - 1].time, { hour: 'numeric', minute: 'numeric' }) }}</span>
</div>
</div>
</div>
</div>
</template>
<style lang=scss>
.chat-log {
.chat-body {
max-inline-size: calc(100% - 6.75rem);
.chat-content {
border-end-end-radius: 6px;
border-end-start-radius: 6px;
p {
overflow-wrap: anywhere;
}
&.chat-left {
border-start-end-radius: 6px;
}
&.chat-right {
border-start-start-radius: 6px;
}
}
}
}
</style>

View File

@@ -0,0 +1,214 @@
<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 isAuthenticationEnabled = 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"
color="disabled"
icon="tabler-x"
/>
</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-3"
>
<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-capitalize text-medium-emphasis mb-0">
{{ store.profileUser.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content pb-5 px-6"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-6 text-medium-emphasis">
<div
for="textarea-user-about"
class="text-base text-disabled"
>
ABOUT
</div>
<AppTextarea
id="textarea-user-about"
v-model="store.profileUser.about"
auto-grow
class="mt-1"
rows="3"
/>
</div>
<!-- Status -->
<div class="mb-6">
<div class="text-base text-disabled">
STATUS
</div>
<VRadioGroup
v-model="store.profileUser.status"
class="mt-1"
>
<VRadio
v-for="(radioOption, index) in userStatusRadioOptions"
:id="`${index}`"
:key="radioOption.title"
:name="radioOption.title"
:label="radioOption.title"
:value="radioOption.value"
:color="radioOption.color"
/>
</VRadioGroup>
</div>
<!-- Settings -->
<div class="text-medium-emphasis chat-settings-section">
<div class="text-base text-disabled">
SETTINGS
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2 text-high-emphasis"
icon="tabler-lock"
size="22"
/>
<div class="text-high-emphasis d-flex align-center justify-space-between flex-grow-1">
<div class="text-body-1 text-high-emphasis">
Two-step Verification
</div>
<VSwitch
id="two-step-verification"
v-model="isAuthenticationEnabled"
density="compact"
/>
</div>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2 text-high-emphasis"
icon="tabler-bell"
size="22"
/>
<div class="text-high-emphasis d-flex align-center justify-space-between flex-grow-1">
<div class="text-body-1 text-high-emphasis">
Notification
</div>
<VSwitch
id="chat-notification"
v-model="isNotificationEnabled"
density="compact"
/>
</div>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2 text-high-emphasis"
icon="tabler-user-plus"
size="22"
/>
<div class="text-body-1 text-high-emphasis">
Invite Friends
</div>
</div>
<div class="d-flex align-center pa-2">
<VIcon
class="me-2 text-high-emphasis"
icon="tabler-trash"
size="22"
/>
<div class="text-body-1 text-high-emphasis">
Delete Account
</div>
</div>
</div>
<!-- Logout Button -->
<VBtn
color="primary"
class="mt-12"
block
append-icon="tabler-logout"
>
Logout
</VBtn>
</PerfectScrollbar>
</template>
</template>
<style lang="scss">
.chat-settings-section {
.v-switch {
.v-input__control {
.v-selection-control__wrapper {
block-size: 18px;
}
}
}
}
</style>

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,281 @@
<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: 'Enter a category description...' }),
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()
}
</script>
<template>
<VNavigationDrawer
:model-value="props.isDrawerOpen"
temporary
location="end"
width="370"
border="none"
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">
<AppTextField
v-model="categoryTitle"
label="Title"
:rules="[requiredValidator]"
placeholder="Fashion"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="categorySlug"
label="Slug"
:rules="[requiredValidator]"
placeholder="Trends fashion"
/>
</VCol>
<VCol cols="12">
<VLabel>
<span class="text-sm text-high-emphasis mb-1">Attachment</span>
</VLabel>
<VFileInput
v-model="categoryImg"
prepend-icon=""
:rules="[requiredValidator]"
clearable
>
<template #append>
<VBtn variant="tonal">
Choose
</VBtn>
</template>
</VFileInput>
</VCol>
<VCol cols="12">
<AppSelect
v-model="parentCategory"
:rules="[requiredValidator]"
label="Parent Category"
placeholder="Select Parent Category"
:items="['HouseHold', 'Management', 'Electronics', 'Office', 'Accessories']"
/>
</VCol>
<VCol cols="12">
<p class="text-body-2 text-high-emphasis mb-1">
Description
</p>
<div class="border rounded px-3 py-1">
<EditorContent :editor="editor" />
<div
v-if="editor"
class="d-flex justify-end flex-wrap gap-x-2"
>
<VIcon
icon="tabler-bold"
:color="editor.isActive('bold') ? 'primary' : ''"
size="20"
@click="editor.chain().focus().toggleBold().run()"
/>
<VIcon
:color="editor.isActive('underline') ? 'primary' : ''"
icon="tabler-underline"
size="20"
@click="editor.commands.toggleUnderline()"
/>
<VIcon
:color="editor.isActive('italic') ? 'primary' : ''"
icon="tabler-italic"
size="20"
@click="editor.chain().focus().toggleItalic().run()"
/>
<VIcon
:color="editor.isActive('bulletList') ? 'primary' : ''"
icon="tabler-list"
size="20"
@click="editor.chain().focus().toggleBulletList().run()"
/>
<VIcon
:color="editor.isActive('orderedList') ? 'primary' : ''"
icon="tabler-list-numbers"
size="20"
@click="editor.chain().focus().toggleOrderedList().run()"
/>
<VIcon
icon="tabler-link"
size="20"
@click="setLink"
/>
<VIcon
icon="tabler-photo"
size="20"
@click="addImage"
/>
</div>
</div>
</VCol>
<VCol cols="12">
<AppSelect
v-model="parentStatus"
:rules="[requiredValidator]"
placeholder="Select Category Status"
label="Select Category 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="tonal"
@click="resetForm"
>
Discard
</VBtn>
</div>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>
<style lang="scss">
.category-navigation-drawer {
.ProseMirror {
min-block-size: 9vh !important;
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;
}
&-focused {
outline: 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));
}
}
</style>

View File

@@ -0,0 +1,211 @@
<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)
}
const closeNavigationDrawer = () => {
emit('update:isDrawerOpen', false)
nextTick(() => {
refVForm.value?.reset()
refVForm.value?.resetValidation()
})
}
</script>
<template>
<VNavigationDrawer
data-allow-mismatch
:model-value="props.isDrawerOpen"
temporary
location="end"
width="370"
border="none"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Add a Customer"
@cancel="closeNavigationDrawer"
/>
<VDivider />
<VCard flat>
<PerfectScrollbar
:options="{ wheelPropagation: false }"
class="h-100"
>
<VCardText style="block-size: calc(100vh - 5rem);">
<VForm
ref="refVForm"
@submit.prevent=""
>
<VRow>
<VCol>
<h6 class="text-h6">
Basic Information
</h6>
</VCol>
<VCol cols="12">
<AppTextField
v-model="name"
label="Name*"
:rules="[requiredValidator]"
placeholder="John Doe"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="email"
label="Email*"
:rules="[requiredValidator, emailValidator]"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="mobile"
label="Mobile*"
: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">
<AppTextField
v-model="addressline1"
label="Address Line 1*"
:rules="[requiredValidator]"
placeholder="45, Rocker Terrace"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="addressline2"
placeholder="Empire Heights,"
:rules="[requiredValidator]"
label="Address Line 2*"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="town"
label="Town*"
:rules="[requiredValidator]"
placeholder="New York"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="state"
placeholder="Texas"
:rules="[requiredValidator]"
label="State/Province*"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="postCode"
label="Post Code*"
type="number"
:rules="[requiredValidator]"
placeholder="982347"
/>
</VCol>
<VCol cols="12">
<AppSelect
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>
<div class="text-body-2">
Please check budget for more info
</div>
</div>
<VSwitch v-model="isBillingAddress" />
</div>
</VCol>
<VCol cols="12">
<div class="d-flex gap-4 justify-start pb-10">
<VBtn
type="submit"
color="primary"
>
Add
</VBtn>
<VBtn
color="error"
variant="tonal"
@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,235 @@
<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)
const customerData = {
id: props.customerData.id,
fullName: props.customerData.customer,
firstName: props.customerData.customer.split(' ')[0],
lastName: props.customerData.customer.split(' ')[1],
company: '',
role: '',
username: props.customerData.customer,
country: props.customerData.country,
contact: props.customerData.contact,
email: props.customerData.email,
currentPlan: '',
status: props.customerData.status,
avatar: '',
taskDone: null,
projectDone: null,
taxId: 'Tax-8894',
language: 'English',
}
</script>
<template>
<VRow>
<!-- SECTION Customer Details -->
<VCol cols="12">
<VCard v-if="props.customerData">
<VCardText class="text-center pt-12">
<!-- 👉 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"
>
{{ avatarText(props.customerData.customer) }}
</span>
</VAvatar>
<!-- 👉 Customer fullName -->
<h5 class="text-h5 mt-4">
{{ props.customerData.customer }}
</h5>
<div class="text-body-1">
Customer ID #{{ props.customerData.customerId }}
</div>
<div class="d-flex justify-space-evenly gap-x-5 mt-6">
<div class="d-flex align-center">
<VAvatar
variant="tonal"
color="primary"
rounded
class="me-4"
>
<VIcon icon="tabler-shopping-cart" />
</VAvatar>
<div class="d-flex flex-column align-start">
<h5 class="text-h5">
{{ props.customerData.order }}
</h5>
<div class="text-body-1">
Order
</div>
</div>
</div>
<div class="d-flex align-center">
<VAvatar
variant="tonal"
color="primary"
rounded
class="me-3"
>
<VIcon icon="tabler-currency-dollar" />
</VAvatar>
<div class="d-flex flex-column align-start">
<h5 class="text-h5">
${{ props.customerData.totalSpent }}
</h5>
<div class="text-body-1">
Spent
</div>
</div>
</div>
</div>
</VCardText>
<!-- 👉 Customer Details -->
<VCardText>
<h5 class="text-h5">
Details
</h5>
<VDivider class="my-4" />
<VList class="card-list mt-2">
<VListItem>
<h6 class="text-h6">
Username:
<span class="text-body-1 d-inline-block">
{{ props.customerData.customer }}
</span>
</h6>
</VListItem>
<VListItem>
<h6 class="text-h6">
Billing Email:
<span class="text-body-1 d-inline-block">
{{ props.customerData.email }}
</span>
</h6>
</VListItem>
<VListItem>
<div class="d-flex gap-x-2 align-center">
<h6 class="text-h6">
Status:
</h6>
<VChip
label
color="success"
size="small"
>
{{ props.customerData.status }}
</VChip>
</div>
</VListItem>
<VListItem>
<h6 class="text-h6">
Contact:
<span class="text-body-1 d-inline-block">
{{ props.customerData.contact }}
</span>
</h6>
</VListItem>
<VListItem>
<h6 class="text-h6">
Country:
<span class="text-body-1 d-inline-block">
{{ props.customerData.country }}
</span>
</h6>
</VListItem>
</VList>
</VCardText>
<VCardText class="text-center">
<VBtn
block
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
>
Edit Details
</VBtn>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
<!-- SECTION Upgrade to Premium -->
<VCol cols="12">
<VCard
flat
class="current-plan"
>
<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:is-dialog-visible="isUserInfoEditDialogVisible"
:user-data="customerData"
/>
<UserUpgradePlanDialog v-model:is-dialog-visible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 0.5rem;
}
.current-plan {
background: linear-gradient(45deg, rgb(var(--v-theme-primary)) 0%, #9e95f5 100%);
color: #fff;
}
</style>

View File

@@ -0,0 +1,161 @@
<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 => {
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 justify-space-between flex-wrap align-center gap-4">
<h5 class="text-h5">
Orders placed
</h5>
<div>
<AppTextField
v-model="searchQuery"
placeholder="Search Order"
style=" max-inline-size: 200px; min-inline-size: 200px;"
/>
</div>
</div>
</VCardText>
<VDivider />
<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"
@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
label
:color="resolveStatus(item.status)?.color"
size="small"
>
{{ item.status }}
</VChip>
</template>
<!-- Spent -->
<template #item.spent="{ item }">
${{ item.spent }}
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
<VMenu activator="parent">
<VList>
<VListItem
value="view"
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }"
>
View
</VListItem>
<VListItem
value="delete"
@click="deleteOrder(item.id)"
>
Delete
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
<!-- pagination -->
<template #bottom>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalOrder"
/>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,396 @@
<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 currentCardDetails = {
number: '1234567890123456',
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,
city: 'Brisbane',
}
const show = ref([
false,
true,
false,
])
const paymentShow = ref([
false,
true,
false,
])
const isEditAddressDialogVisible = ref(false)
const isCardAddDialogVisible = ref(false)
const isNewEditAddressDialogVisible = ref(false)
const isNewCardAddDialogVisible = ref(false)
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: 'Expires Apr 2028',
isDefaultMethod: false,
image: mastercard,
},
{
title: 'American Express',
subtitle: 'Expires 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-6 flex-wrap align-center gap-y-4 gap-x-6">
<h5 class="text-h5">
Address Book
</h5>
<VBtn
variant="tonal"
size="small"
@click="isNewEditAddressDialogVisible = !isNewEditAddressDialogVisible"
>
Add new Address
</VBtn>
</div>
<template
v-for="(address, index) in addressData"
:key="index"
>
<div>
<div class="d-flex justify-space-between py-3 gap-y-2 flex-wrap align-center">
<div class="d-flex align-center gap-x-4">
<VIcon
:icon="show[index] ? 'tabler-chevron-down' : 'tabler-chevron-right'"
class="flip-in-rtl text-high-emphasis"
size="24"
@click="show[index] = !show[index]"
/>
<div>
<div class="d-flex align-center gap-x-2 mb-1">
<h6 class="text-h6">
{{ address.title }}
</h6>
<VChip
v-if="address.defaultAddress"
color="success"
label
size="small"
>
Default Address
</VChip>
</div>
<div class="text-body-1">
{{ address.subtitle }}
</div>
</div>
</div>
<div class="ms-5">
<IconBtn @click="isEditAddressDialogVisible = !isEditAddressDialogVisible">
<VIcon
icon="tabler-edit"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-trash"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-dots-vertical"
class="flip-in-rtl"
/>
</IconBtn>
</div>
</div>
<VExpandTransition>
<div v-show="show[index]">
<div class="px-10 pb-3">
<h6 class="mb-1 text-h6">
{{ address.owner }}
</h6>
<div
class="text-body-1"
v-html="address.address"
/>
</div>
</div>
</VExpandTransition>
<VDivider v-if="index !== addressData.length - 1" />
</div>
</template>
</VCardText>
</VCard>
<!-- 👉 Payment Methods -->
<VCard>
<VCardText>
<div class="d-flex justify-space-between mb-6 flex-wrap align-center gap-y-4 gap-x-6">
<h5 class="text-h5">
Payment Methods
</h5>
<VBtn
variant="tonal"
size="small"
@click="isNewCardAddDialogVisible = !isNewCardAddDialogVisible"
>
Add Payment Methods
</VBtn>
</div>
<template
v-for="(payment, index) in paymentData"
:key="index"
>
<div>
<div class="d-flex justify-space-between py-3 gap-y-2 flex-wrap align-center">
<div class="d-flex align-center gap-x-4">
<VIcon
:icon="paymentShow[index] ? 'tabler-chevron-down' : 'tabler-chevron-right'"
size="24"
class="flip-in-rtl text-high-emphasis"
@click="paymentShow[index] = !paymentShow[index]"
/>
<VImg
:src="payment.image"
height="30"
width="50"
/>
<div>
<div class="d-flex gap-x-2 mb-1">
<h6 class="text-h6">
{{ payment.title }}
</h6>
<VChip
v-if="payment.isDefaultMethod"
color="success"
label
size="small"
>
Default Method
</VChip>
</div>
<div class="text-body-1">
{{ payment.subtitle }}
</div>
</div>
</div>
<div class="ms-5">
<IconBtn @click="isCardAddDialogVisible = !isCardAddDialogVisible">
<VIcon
icon="tabler-edit"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-trash"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-dots-vertical"
class="flip-in-rtl"
/>
</IconBtn>
</div>
</div>
<VExpandTransition>
<div v-show="paymentShow[index]">
<div class="px-10 pb-3">
<VRow>
<VCol
cols="12"
md="6"
>
<VTable>
<tr>
<td>Name </td>
<td class="font-weight-medium text-high-emphasis">
Violet Mendoza
</td>
</tr>
<tr>
<td>Number </td>
<td class="font-weight-medium text-high-emphasis">
**** 4487
</td>
</tr>
<tr>
<td>Expires </td>
<td class="font-weight-medium text-high-emphasis">
08/2028
</td>
</tr>
<tr>
<td>Type </td>
<td class="font-weight-medium text-high-emphasis">
Master Card
</td>
</tr>
<tr>
<td>Issuer </td>
<td class="font-weight-medium text-high-emphasis">
VICBANK
</td>
</tr>
<tr>
<td>ID </td>
<td class="font-weight-medium text-high-emphasis">
DH73DJ8
</td>
</tr>
</VTable>
</VCol>
<VCol
cols="12"
md="6"
>
<VTable>
<tr>
<td>Billing </td>
<td class="font-weight-medium text-high-emphasis">
United Kingdom
</td>
</tr>
<tr>
<td>Number</td>
<td class="font-weight-medium text-high-emphasis">
+7634 983 637
</td>
</tr>
<tr>
<td>Email</td>
<td class="font-weight-medium text-high-emphasis">
vafgot@vultukir.org
</td>
</tr>
<tr>
<td>Origin</td>
<td class="d-flex">
<div class="me-2 font-weight-medium text-high-emphasis">
United States
</div>
<img
:src="usFlag"
height="20"
width="20"
>
</td>
</tr>
<tr>
<td>CVC Check</td>
<td class="d-flex">
<div class="me-2 font-weight-medium text-high-emphasis">
Passed
</div>
<VAvatar
variant="tonal"
color="success"
size="20"
inline
>
<VIcon
icon="tabler-check"
color="success"
size="12"
/>
</VAvatar>
</td>
</tr>
</VTable>
</VCol>
</VRow>
</div>
</div>
</VExpandTransition>
<VDivider v-if="index !== paymentData.length - 1" />
</div>
</template>
</VCardText>
</VCard>
<AddEditAddressDialog
v-model:is-dialog-visible="isEditAddressDialogVisible"
:billing-address="editBillingData"
/>
<AddEditAddressDialog v-model:is-dialog-visible="isNewEditAddressDialogVisible" />
<CardAddEditDialog
v-model:is-dialog-visible="isCardAddDialogVisible"
:card-details="currentCardDetails"
/>
<CardAddEditDialog v-model:is-dialog-visible="isNewCardAddDialogVisible" />
</template>

View File

@@ -0,0 +1,93 @@
<script setup>
const notifications = ref([
{
type: 'New for you',
email: true,
browser: true,
app: true,
},
{
type: 'Account activity',
email: true,
browser: true,
app: true,
},
{
type: 'A new browser used to sign in',
email: true,
browser: true,
app: false,
},
{
type: 'A new device is linked',
email: true,
browser: false,
app: false,
},
])
</script>
<template>
<VCard class="user-tab-notification">
<VCardItem>
<VCardTitle class="mb-1">
Notifications
</VCardTitle>
<VCardSubtitle class="text-body-1 text-wrap">
You will receive notification for the below selected items.
</VCardSubtitle>
</VCardItem>
<VCardText class="px-0">
<VDivider />
<VTable class="text-no-wrap">
<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, index) in notifications"
:key="notification.type"
:class="index % 2 === 0 ? 'table-colored-raw' : ''"
>
<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>
<VCardText class="d-flex flex-wrap gap-4">
<VBtn>Save changes</VBtn>
<VBtn
color="secondary"
variant="tonal"
>
Discard
</VBtn>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,127 @@
<script setup>
import CustomerOrderTable from './CustomerOrderTable.vue'
</script>
<template>
<VRow>
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText class="d-flex gap-y-2 flex-column">
<VAvatar
variant="tonal"
color="primary"
icon="tabler-currency-dollar"
rounded
/>
<h5 class="text-h5">
Account Balance
</h5>
<div>
<h5 class="text-h5 text-primary mb-1">
$7480
<span class="text-body-1 d-inline-block">Credit Left</span>
</h5>
<p class="mb-0">
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="tabler-gift"
rounded
/>
<h5 class="text-h5">
Loyalty Program
</h5>
<div>
<VChip
color="success"
class="mb-2"
label
size="small"
>
Platinum member
</VChip>
<p class="mb-0">
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="tabler-star"
rounded
/>
<h5 class="text-h5">
Wishlist
</h5>
<div>
<h5 class="text-h5 text-warning mb-1">
15
<span class="text-body-1 d-inline-block">Items in wishlist</span>
</h5>
<p class="mb-0">
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="tabler-discount"
rounded
/>
<h5 class="text-h5">
Coupons
</h5>
<div>
<h5 class="text-h5 text-info mb-1">
21
<span class="text-body-1 d-inline-block">Coupons you win</span>
</h5>
<p class="mb-0">
Use coupon on next purchase
</p>
</div>
</VCardText>
</VCard>
</VCol>
<VCol>
<CustomerOrderTable />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,203 @@
<script setup>
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: 'tabler-brand-windows',
color: 'info',
device: 'HP Specter 360',
location: 'Switzerland',
activity: '10, July 2021 20:07',
},
{
browser: 'Chrome on iPhone',
logo: 'tabler-device-mobile',
color: 'error',
device: 'iPhone 12x',
location: 'Australia',
activity: '13, July 2021 10:10',
},
{
browser: 'Chrome on Android',
logo: 'tabler-brand-android',
color: 'success',
device: 'OnePlus 9 Pro',
location: 'Dubai',
activity: '4, July 2021 15:15',
},
{
browser: 'Chrome on macOS',
logo: 'tabler-brand-apple',
color: 'secondary',
device: 'Apple iMac',
location: 'India',
activity: '20, July 2021 21:01',
},
{
browser: 'Chrome on Windows',
logo: 'tabler-brand-windows',
color: 'info',
device: 'HP Specter 360',
location: 'Switzerland',
activity: '10, July 2021 20:07',
},
{
browser: 'Chrome on Android',
logo: 'tabler-brand-android',
color: 'success',
device: 'OnePlus 9 Pro',
location: 'Dubai',
activity: '4, July 2021 15:15',
},
]
</script>
<template>
<VRow>
<VCol cols="12">
<!-- 👉 Change password -->
<VCard title="Change Password">
<VCardText>
<VAlert
variant="tonal"
color="warning"
title="Ensure that these requirements are met"
text="Minimum 8 characters long, uppercase & symbol"
class="mb-4"
closable
/>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
label="New Password"
placeholder="············"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Confirm Password"
autocomplete="confirm-password"
placeholder="············"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@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-steps verification"
subtitle="Keep your account secure with authentication step."
>
<VCardText>
<div class="text-h6 mb-1">
SMS
</div>
<AppTextField placeholder="+1(968) 819-2547">
<template #append>
<IconBtn>
<VIcon
icon="tabler-edit"
size="22"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-user-plus"
size="22"
/>
</IconBtn>
</template>
</AppTextField>
<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">
<VDivider />
<VDataTable
:items="recentDevices"
:headers="recentDeviceHeader"
hide-default-footer
class="text-no-wrap"
>
<template #item.browser="{ item }">
<div class="d-flex align-center gap-x-4">
<VIcon
:icon="item.logo"
:color="item.color"
size="22"
/>
<div class="text-body-1 text-high-emphasis">
{{ item.browser }}
</div>
</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:is-dialog-visible="isTwoFactorDialogOpen"
:sms-code="smsVerificationNumber"
/>
</template>

View File

@@ -0,0 +1,141 @@
<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
type="info"
variant="tonal"
>
<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 class="customer-info-card">
<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
variant="tonal"
color="secondary"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>
<style lang="scss" scoped>
.customer-info-card {
.v-radio-group {
.v-input__control {
.v-label {
margin-inline-start: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,119 @@
<script setup>
const isFullfilOnline = ref(true)
</script>
<template>
<div>
<VCard
title="Location Name"
class="mb-6"
>
<VCardText>
<AppTextField
label="Location Name"
placeholder="Empire Hub"
/>
<div class="my-4">
<VCheckbox
v-model="isFullfilOnline"
label="Fulfil online orders from this location"
/>
</div>
<VAlert
type="info"
variant="tonal"
>
<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">
<AppSelect
label="Country/religion"
placeholder="Select Country"
:items="['United States', 'UK', 'Canada']"
model-value="United States"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="Address"
placeholder="123 , New Street"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="Apartment, suite, etc."
placeholder="Empire Heights"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="Phone"
placeholder="+1 (234) 456-7890"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="City"
placeholder="New York"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="State"
placeholder="NY"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="PIN code"
type="number"
placeholder="123897"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4 mt-6">
<VBtn
variant="tonal"
color="secondary"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</div>
</template>

View File

@@ -0,0 +1,201 @@
<script setup>
const customerNotifications = ref([
{
type: 'New customer sign up',
email: true,
app: true,
},
{
type: 'Customer account password reset',
email: true,
app: true,
},
{
type: 'Customer account invite',
email: false,
app: false,
},
])
const shippingNotifications = ref([
{
type: 'Picked up',
email: true,
app: true,
},
{
type: 'Shipping update ',
email: true,
app: false,
},
{
type: 'Delivered',
email: false,
app: true,
},
])
const ordersNotification = ref([
{
type: 'Order purchase',
email: true,
app: true,
},
{
type: 'Order cancelled',
email: true,
app: false,
},
{
type: 'Order refund request',
email: false,
app: true,
},
{
type: 'Order confirmation',
email: true,
app: false,
},
{
type: 'Payment error',
email: true,
app: false,
},
])
</script>
<template>
<VCard class="mb-4">
<VCardText>
<h5 class="text-h5 mb-2">
Customer
</h5>
<VTable class="text-no-wrap mb-6 border rounded">
<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"
class="text-high-emphasis"
>
{{ 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-2">
Orders
</h5>
<VTable class="text-no-wrap mb-6 border rounded">
<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"
class="text-high-emphasis"
>
{{ 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-2">
Shipping
</h5>
<VTable class="text-no-wrap mb-6 border rounded">
<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"
class="text-high-emphasis"
>
{{ 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
variant="tonal"
color="secondary"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1,158 @@
<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>
<div class="text-body-1 mb-5">
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.
</div>
<VBtn
variant="tonal"
@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="my-class mb-5">
<div class="d-flex justify-space-between align-center mb-6">
<div class="rounded paypal-logo">
<img
:src="paypal"
alt="Pixinvent"
style="padding-block: 6px;padding-inline: 18px;"
>
</div>
<VBtn variant="text">
Activate PayPal
</VBtn>
</div>
<VDivider />
<div class="d-flex justify-space-between flex-wrap mt-6 gap-4">
<div>
<div
class="text-body-2 mb-2"
style="min-inline-size: 220px;"
>
Provider
</div>
<h6 class="text-h6">
PayPal
</h6>
</div>
<div>
<div
class="text-body-2 mb-2"
style="min-inline-size: 220px;"
>
Status
</div>
<VChip
color="warning"
size="small"
label
>
Inactive
</VChip>
</div>
<div>
<div
class="text-body-2 mb-2"
style="min-inline-size: 220px;"
>
Transaction Fee
</div>
<h6 class="text-h6">
2.99%
</h6>
</div>
</div>
</div>
<VBtn
variant="tonal"
@click="isAddPaymentMethodsDialogVisible = !isAddPaymentMethodsDialogVisible"
>
Add Payment Methods
</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>
<VBtn
variant="tonal"
:append-icon="$vuetify.display.smAndUp ? 'tabler-chevron-down' : ''"
>
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="tonal"
>
Discard
</VBtn>
<VBtn color="primary">
save changes
</VBtn>
</div>
</div>
<AddPaymentMethodDialog v-model:is-dialog-visible="isAddPaymentMethodsDialogVisible" />
<PaymentProvidersDialog v-model:is-dialog-visible="isPaymentProvidersDialogVisible" />
</template>
<style lang="scss" scoped>
.paypal-logo {
background-color: #fff;
block-size: 37px;
box-shadow: 0 2px 4px 0 rgba(165, 163, 174, 30%);
inline-size: 58px;
}
</style>

View File

@@ -0,0 +1,186 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import usflag 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-2"
/>
<div>
<h6 class="text-h6">
Domestic
</h6>
<div class="text-body-2">
United state of America
</div>
</div>
<VSpacer />
<div>
<IconBtn color="secondary">
<VIcon icon="tabler-pencil" />
</IconBtn>
<IconBtn color="secondary">
<VIcon icon="tabler-trash" />
</IconBtn>
</div>
</div>
<VTable class="mb-4 border rounded">
<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>
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
<VBtn variant="tonal">
Add rate
</VBtn>
</div>
<div>
<div class="d-flex flex-wrap align-center mb-4">
<VAvatar
:image="usflag"
size="30"
class="me-2"
/>
<div>
<h6 class="text-h6">
International
</h6>
<div class="text-body-2">
United state of America
</div>
</div>
<VSpacer />
<div>
<IconBtn color="secondary">
<VIcon icon="tabler-pencil" />
</IconBtn>
<IconBtn color="secondary">
<VIcon icon="tabler-trash" />
</IconBtn>
</div>
</div>
<VTable class="mb-4 border rounded">
<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>
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
<VBtn variant="tonal">
Add rate
</VBtn>
</div>
</VCardText>
</VCard>
<div class="d-flex justify-end gap-x-4">
<VBtn
variant="tonal"
color="secondary"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

@@ -0,0 +1,244 @@
<template>
<VCard
title="Profile"
class="mb-6"
>
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Store name"
placeholder="Pixinvent"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Phone"
placeholder="+(123) 456-7890"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Store contact email"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Sender email"
placeholder="johndoe@email.com"
/>
</VCol>
<VCol>
<VAlert
color="warning"
variant="tonal"
icon="tabler-bell"
>
<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"
>
<AppTextField
label="Legal business name"
placeholder="Pixinvent"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppSelect
label="Country/Region"
:items="['United States', 'Canada', 'UK']"
placeholder="Canada"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
placeholder="126, New Street"
label="Address"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Apartment,suit, etc."
placeholder="Empire Heights"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="City"
placeholder="New York"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="State"
placeholder="NY"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<AppTextField
label="PIN Code"
placeholder="111011"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<VCard class="mb-6">
<template #title>
<div class="v-card-title text-wrap">
Time zone and units of measurement
</div>
</template>
<template #subtitle>
<div class="text-wrap">
Used to calculate product prices, shipping weights, and order times.
</div>
</template>
<VCardText>
<VRow>
<VCol cols="12">
<AppSelect
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"
>
<AppSelect
label="Unit system"
:items="['Metric System', 'Imperial', 'International System']"
placeholder="Metric System"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppSelect
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>
<AppSelect
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"
>
<AppTextField
label="Prefix"
prefix="#"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
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
variant="tonal"
color="secondary"
>
Discard
</VBtn>
<VBtn>Save Changes</VBtn>
</div>
</template>

View File

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

View File

@@ -0,0 +1,246 @@
<script setup>
const emit = defineEmits(['close'])
const content = ref('')
const to = ref('')
const subject = ref('')
const message = ref('')
const cc = ref('')
const bcc = ref('')
const isEmailCc = ref(false)
const isEmailBcc = ref(false)
const resetValues = () => {
to.value = subject.value = message.value = ''
}
</script>
<template>
<VCard
class="email-compose-dialog"
elevation="10"
max-width="30vw"
>
<VCardItem class="py-3 px-6">
<div class="d-flex align-center">
<h5 class="text-h5">
Compose Mail
</h5>
<VSpacer />
<div class="d-flex align-center gap-x-2">
<IconBtn
size="small"
icon="tabler-minus"
@click="$emit('close')"
/>
<IconBtn
size="small"
icon="tabler-x"
@click="$emit('close'); resetValues(); isEmailCc = false; isEmailBcc = false;"
/>
</div>
</div>
</VCardItem>
<div class="px-1 pe-6 py-1">
<VTextField
v-model="to"
density="compact"
>
<template #prepend-inner>
<div class="text-base font-weight-medium text-disabled">
To:
</div>
</template>
<template #append>
<span class="cursor-pointer">
<span @click="isEmailCc = !isEmailCc">Cc</span>
<span> | </span>
<span @click="isEmailBcc = !isEmailBcc">Bcc</span>
</span>
</template>
</VTextField>
</div>
<VExpandTransition>
<div v-if="isEmailCc">
<VDivider />
<div class="px-1 pe-6 py-1">
<VTextField
v-model="cc"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
Cc:
</div>
</template>
</VTextField>
</div>
</div>
</VExpandTransition>
<VExpandTransition>
<div v-if="isEmailBcc">
<VDivider />
<div class="px-1 pe-6 py-1">
<VTextField
v-model="bcc"
density="compact"
>
<template #prepend-inner>
<div class="text-disabled font-weight-medium">
Bcc:
</div>
</template>
</VTextField>
</div>
</div>
</VExpandTransition>
<VDivider />
<div class="px-1 pe-6 py-1">
<VTextField
v-model="subject"
density="compact"
>
<template #prepend-inner>
<div class="text-base font-weight-medium text-disabled">
Subject:
</div>
</template>
</VTextField>
</div>
<VDivider />
<!-- 👉 Tiptap Editor -->
<TiptapEditor
v-model="content"
placeholder="Message"
/>
<div class="d-flex align-center px-6 py-4">
<VBtn
color="primary"
class="me-4"
append-icon="tabler-send"
:disabled="to === '' ? true : false"
@click="$emit('close'); content = ''; resetValues(); isEmailCc = false; isEmailBcc = false;"
>
send
</VBtn>
<IconBtn size="small">
<VIcon icon="tabler-paperclip" />
</IconBtn>
<VSpacer />
<IconBtn
size="small"
class="me-2"
>
<VIcon icon="tabler-dots-vertical" />
</IconBtn>
<IconBtn
size="small"
@click="$emit('close'); resetValues(); content = ''; isEmailCc = false; isEmailBcc = false;"
>
<VIcon icon="tabler-trash" />
</IconBtn>
</div>
</VCard>
</template>
<style lang="scss">
@use "@core-scss/base/mixins";
.v-card.email-compose-dialog {
z-index: 910 !important;
@include mixins.elevation(18);
.v-field--prepended {
padding-inline-start: 20px;
}
.v-field__prepend-inner {
align-items: center;
padding: 0;
}
.v-field__prepend-inner {
align-items: center;
padding: 0;
}
.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;
}
.v-input {
.v-field__prepend-inner {
display: flex;
align-items: center;
padding-block-start: 0;
}
}
.app-text-field {
.v-field__input {
padding-block-start: 6px;
}
.v-field--focused {
box-shadow: none !important;
}
}
}
.email-compose-dialog {
.ProseMirror {
p {
margin-block-end: 0;
}
padding: 1.5rem;
block-size: 100px;
overflow-y: auto;
padding-block: 0.5rem;
&:focus-visible {
outline: none;
}
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;
}
&-focused {
outline: none;
}
}
}
</style>

View File

@@ -0,0 +1,261 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
emailsMeta: {
type: Object,
required: true,
},
})
const emit = defineEmits(['toggleComposeDialogVisibility'])
defineOptions({
inheritAttrs: false,
})
const inboxEmails = ref(0)
const draftEmails = ref(0)
const spamEmails = ref(0)
const starredEmails = ref(0)
watch(() => props.emailsMeta, emailsMeta => {
if (!emailsMeta)
return
inboxEmails.value = emailsMeta.inbox
draftEmails.value = emailsMeta.draft
spamEmails.value = emailsMeta.spam
starredEmails.value = emailsMeta.star
}, {
immediate: true,
deep: true,
})
const folders = computed(() => [
{
title: 'Inbox',
prependIcon: 'tabler-mail',
to: { name: 'apps-email' },
badge: {
content: inboxEmails.value,
color: 'primary',
},
},
{
title: 'Sent',
prependIcon: 'tabler-send',
to: {
name: 'apps-email-filter',
params: { filter: 'sent' },
},
},
{
title: 'Draft',
prependIcon: 'tabler-edit',
to: {
name: 'apps-email-filter',
params: { filter: 'draft' },
},
badge: {
content: draftEmails.value,
color: 'warning',
},
},
{
title: 'Starred',
prependIcon: 'tabler-star',
to: {
name: 'apps-email-filter',
params: { filter: 'starred' },
},
badge: {
content: starredEmails.value,
color: 'success',
},
},
{
title: 'Spam',
prependIcon: 'tabler-alert-circle',
to: {
name: 'apps-email-filter',
params: { filter: 'spam' },
},
badge: {
content: spamEmails.value,
color: 'error',
},
},
{
title: 'Trash',
prependIcon: 'tabler-trash',
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="px-6 pb-5 pt-6">
<VBtn
block
@click="$emit('toggleComposeDialogVisibility')"
>
Compose
</VBtn>
</div>
<!-- 👉 Folders -->
<PerfectScrollbar
:options="{ wheelPropagation: false }"
class="h-100"
>
<!-- Filters -->
<ul class="email-filters py-4">
<RouterLink
v-for="folder in folders"
:key="folder.title"
v-slot="{ isActive, href, navigate }"
class="d-flex align-center cursor-pointer align-center"
: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"
/>
<div class="text-base">
{{ folder.title }}
</div>
<VSpacer />
<VChip
v-if="folder.badge?.content"
:color="folder.badge.color"
label
size="small"
class="rounded-xl px-3"
>
{{ folder.badge?.content }}
</VChip>
</li>
</RouterLink>
</ul>
<ul class="email-labels py-4">
<!-- 👉 Labels -->
<div class="text-caption text-disabled mb-4 px-6">
LABELS
</div>
<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 d-flex align-center"
@click="navigate"
>
<VIcon
icon="tabler-circle-filled"
:color="label.color"
class="me-2"
size="12"
/>
<div class="text-body-1 text-high-emphasis">
{{ label.title }}
</div>
</li>
</RouterLink>
</ul>
</PerfectScrollbar>
</div>
</template>
<style lang="scss">
.email-filters,
.email-labels {
.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;
}
}
}
.email-filters {
> li {
position: relative;
margin-block-end: 4px;
padding-block: 4px;
padding-inline: 24px;
}
}
.email-labels {
> li {
position: relative;
margin-block-end: 0.75rem;
padding-inline: 24px;
}
}
</style>

View File

@@ -0,0 +1,432 @@
<script setup>
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 emailReply = ref('')
const showReplyBox = ref(false)
const showReplyCard = ref(true)
const { updateEmailLabels } = useEmail()
const { labels, resolveLabelColor, emailMoveToFolderActions, shallShowMoveToActionFor, moveSelectedEmailTo } = useEmail()
const handleMoveMailsTo = async action => {
await moveSelectedEmailTo(action, [props.email.id])
emit('refresh')
emit('close')
}
const updateMailLabel = async label => {
await updateEmailLabels([props.email.id], label)
emit('refresh')
}
</script>
<template>
<!-- calc(100% - 256px) => 265px is left sidebar width -->
<VNavigationDrawer
data-allow-mismatch
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-3">
<IconBtn
class="me-2"
@click="$emit('close'); showReplyBox = false; showReplyCard = true; emailReply = ''"
>
<VIcon
size="22"
icon="tabler-chevron-left"
class="flip-in-rtl"
/>
</IconBtn>
<div class="d-flex align-center flex-wrap flex-grow-1 overflow-hidden gap-2">
<div class="text-body-1 text-high-emphasis text-truncate">
{{ props.email.subject }}
</div>
<div class="d-flex flex-wrap gap-2">
<VChip
v-for="label in props.email.labels"
:key="label"
:color="resolveLabelColor(label)"
class="text-capitalize flex-shrink-0"
size="small"
:label="false"
>
{{ label }}
</VChip>
</div>
</div>
<div>
<div class="d-flex align-center">
<IconBtn
:disabled="!props.emailMeta.hasPreviousEmail"
@click="$emit('navigated', 'previous')"
>
<VIcon
icon="tabler-chevron-left"
class="flip-in-rtl"
/>
</IconBtn>
<IconBtn
:disabled="!props.emailMeta.hasNextEmail"
@click="$emit('navigated', 'next')"
>
<VIcon
icon="tabler-chevron-right"
class="flip-in-rtl"
/>
</IconBtn>
</div>
</div>
</div>
<VDivider />
<!-- 👉 Action bar -->
<div class="email-view-action-bar d-flex align-center text-medium-emphasis px-6 gap-x-1">
<!-- Trash -->
<IconBtn
v-show="!props.email.isDeleted"
@click="$emit('trash'); $emit('close')"
>
<VIcon
icon="tabler-trash"
size="22"
/>
<VTooltip
activator="parent"
location="top"
>
Delete Mail
</VTooltip>
</IconBtn>
<!-- Read/Unread -->
<IconBtn @click.stop="$emit('unread'); $emit('close')">
<VIcon
icon="tabler-mail"
size="22"
/>
<VTooltip
activator="parent"
location="top"
>
Mark as Unread
</VTooltip>
</IconBtn>
<!-- Move to folder -->
<IconBtn>
<VIcon
icon="tabler-folder"
size="22"
/>
<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="tabler-tag"
size="22"
/>
<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 />
<div class="d-flex align-center gap-x-1">
<!-- Star/Unstar -->
<IconBtn
:color="props.email.isStarred ? 'warning' : 'default'"
@click="props.email?.isStarred ? $emit('unstar') : $emit('star'); $emit('refresh')"
>
<VIcon icon="tabler-star" />
</IconBtn>
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
</IconBtn>
</div>
</div>
<VDivider />
<!-- 👉 Mail Content -->
<PerfectScrollbar
tag="div"
class="mail-content-container flex-grow-1 pa-sm-12 pa-6"
:options="{ wheelPropagation: false }"
>
<VCard class="mb-4">
<div class="d-flex align-start align-sm-center pa-6 gap-x-4">
<VAvatar size="38">
<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">
<div class="text-body-1 text-high-emphasis text-truncate">
{{ props.email.from.name }}
</div>
<div class="text-sm">
{{ props.email.from.email }}
</div>
</div>
<VSpacer />
<div class="d-flex align-center gap-x-4">
<div class="text-disabled text-base">
{{ new Date(props.email.time).toDateString() }}
</div>
<div>
<IconBtn v-show="props.email.attachments.length">
<VIcon
icon="tabler-paperclip"
size="22"
/>
</IconBtn>
<IconBtn>
<VIcon
icon="tabler-dots-vertical"
size="22"
/>
</IconBtn>
</div>
</div>
</div>
</div>
<VDivider />
<VCardText>
<!-- eslint-disable vue/no-v-html -->
<div class="text-body-1 font-weight-medium text-truncate mb-4">
{{ props.email.from.name }},
</div>
<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 pt-4">
<span>2 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>
<!-- Reply or Forward -->
<VCard v-show="showReplyCard">
<VCardText class="font-weight-medium text-high-emphasis">
<div class="text-base">
Click here to <span
class="text-primary cursor-pointer"
@click="showReplyBox = !showReplyBox; showReplyCard = !showReplyCard"
>
Reply
</span> or <span class="text-primary cursor-pointer">
Forward
</span>
</div>
</VCardText>
</VCard>
<VCard v-if="showReplyBox">
<VCardText>
<h6 class="text-h6 mb-6">
Reply to {{ email?.from.name }}
</h6>
<TiptapEditor
v-model="emailReply"
placeholder="Write your message..."
/>
<div class="d-flex justify-end gap-4 pt-2 flex-wrap">
<VBtn
icon
variant="text"
color="secondary"
@click="showReplyBox = !showReplyBox; showReplyCard = !showReplyCard; emailReply = ''"
>
<VIcon icon="tabler-trash" />
</VBtn>
<VBtn
variant="text"
color="secondary"
>
<template #prepend>
<VIcon
icon="tabler-paperclip"
class="text-high-emphasis"
size="16"
/>
</template>
Attachments
</VBtn>
<VBtn append-icon="tabler-send">
Send
</VBtn>
</div>
</VCardText>
</VCard>
</PerfectScrollbar>
</template>
</VNavigationDrawer>
</template>
<style lang="scss">
.email-view {
&:not(.v-navigation-drawer--active) {
transform: translateX(110%) !important;
}
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;
}
.editor {
padding-block-start: 0 !important;
padding-inline: 0 !important;
}
.ProseMirror {
padding: 0.5rem;
block-size: 100px;
overflow-y: auto;
padding-block: 0.5rem;
}
}
.email-view-action-bar {
min-block-size: 56px;
}
.mail-content-container {
background-color: rgb(var(--v-theme-on-surface), var(--v-hover-opacity));
.mail-header {
margin-block: 12px;
margin-inline: 24px;
}
}
</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: 'tabler-mail' },
{ action: 'spam', icon: 'tabler-alert-octagon' },
{ action: 'trash', icon: 'tabler-trash' },
]
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 = async (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
}
await updateEmails(selectedEmails, dataToUpdate)
}
return {
labels,
resolveLabelColor,
shallShowMoveToActionFor,
emailMoveToFolderActions,
moveSelectedEmailTo,
updateEmails,
updateEmailLabels,
}
}

View File

@@ -0,0 +1,131 @@
<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
data-allow-mismatch
temporary
location="end"
:width="400"
border="none"
: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">
<AppTextField
id="invoice-balance"
v-model="invoiceBalance"
label="Invoice Balance"
type="number"
placeholder="$99"
/>
</VCol>
<VCol cols="12">
<AppTextField
id="payment-amount"
v-model="paymentAmount"
label="Payment Amount"
type="number"
placeholder="$99"
/>
</VCol>
<VCol cols="12">
<AppDateTimePicker
id="invoice-payment-date"
v-model="paymentDate"
label="Payment Date"
placeholder="Select Date"
/>
</VCol>
<VCol cols="12">
<AppSelect
id="invoice-payment-method"
v-model="paymentMethod"
label="Select Payment Method"
placeholder="Select Payment Method"
:items="['Cash', 'Bank Transfer', 'Debit', 'Credit', 'PayPal']"
/>
</VCol>
<VCol cols="12">
<AppTextarea
id="invoice-payment-note"
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="tonal"
@click="$emit('update:isDrawerOpen', false)"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</PerfectScrollbar>
</VNavigationDrawer>
</template>

View File

@@ -0,0 +1,335 @@
<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-6 pa-sm-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 app-logo mb-6">
<!-- 👉 Logo -->
<VNodeRenderer :nodes="themeConfig.app.logo" />
<!-- 👉 Title -->
<h6 class="app-logo-title">
{{ 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 class="d-flex flex-column gap-2">
<!-- 👉 Invoice Id -->
<div class="d-flex align-start align-sm-center gap-x-4 font-weight-medium text-lg flex-column flex-sm-row">
<span
class="text-high-emphasis text-sm-end"
style="inline-size: 5.625rem ;"
>Invoice:</span>
<span>
<AppTextField
id="invoice-id"
v-model="invoice.id"
disabled
prefix="#"
style="inline-size: 9.5rem;"
/>
</span>
</div>
<!-- 👉 Issue Date -->
<div class="d-flex gap-x-4 align-start align-sm-center flex-column flex-sm-row">
<span
class="text-high-emphasis text-sm-end"
style="inline-size: 5.625rem;"
>Date Issued:</span>
<span style="inline-size: 9.5rem;">
<AppDateTimePicker
id="issued-date"
v-model="invoice.issuedDate"
placeholder="YYYY-MM-DD"
:config="{ position: 'auto right' }"
/>
</span>
</div>
<!-- 👉 Due Date -->
<div class="d-flex gap-x-4 align-start align-sm-center flex-column flex-sm-row">
<span
class="text-high-emphasis text-sm-end"
style="inline-size: 5.625rem;"
>Due Date:</span>
<span style="min-inline-size: 9.5rem;">
<AppDateTimePicker
id="due-date"
v-model="invoice.dueDate"
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
id="client-name"
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-4">
Total Due:
</td>
<td>{{ props.data.paymentDetails.totalDue }}</td>
</tr>
<tr>
<td class="pe-4">
Bank Name:
</td>
<td>{{ props.data.paymentDetails.bankName }}</td>
</tr>
<tr>
<td class="pe-4">
Country:
</td>
<td>{{ props.data.paymentDetails.country }}</td>
</tr>
<tr>
<td class="pe-4">
IBAN:
</td>
<td>
<p class="text-wrap me-4">
{{ props.data.paymentDetails.iban }}
</p>
</td>
</tr>
<tr>
<td class="pe-4">
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="tabler-plus"
@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>
<AppTextField
id="salesperson"
v-model="salesperson"
style="inline-size: 8rem;"
placeholder="John Doe"
/>
</div>
<AppTextField
id="thanks-note"
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
id="note"
v-model="note"
placeholder="Write note here..."
:rows="2"
/>
</div>
</VCard>
</template>

View File

@@ -0,0 +1,212 @@
<!-- 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-6 flex-grow-1">
<VRow>
<VCol
cols="12"
md="6"
>
<AppSelect
id="item"
v-model="selectedItem"
:items="itemsOptions"
item-title="title"
item-value="title"
placeholder="Select Item"
class="mb-6"
/>
<AppTextarea
id="item-description"
v-model="localProductData.description"
rows="2"
placeholder="Item description"
persistent-placeholder
/>
</VCol>
<VCol
cols="12"
md="2"
sm="4"
>
<AppTextField
id="item-cost"
v-model="localProductData.cost"
type="number"
placeholder="Cost"
class="mb-6"
/>
<div class="text-high-emphasis text-no-wrap 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"
>
<AppTextField
id="item-hours"
v-model="localProductData.hours"
type="number"
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
size="36"
@click="removeProduct"
>
<VIcon
:size="24"
icon="tabler-x"
/>
</IconBtn>
</div>
</VCard>
</template>

View File

@@ -0,0 +1,141 @@
<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
data-allow-mismatch
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">
<AppTextField
id="sender-email"
v-model="emailFrom"
label="From"
placeholder="sender@email.com"
/>
</VCol>
<VCol cols="12">
<AppTextField
id="receiver-email"
v-model="emailTo"
label="To"
placeholder="receiver@email.com"
/>
</VCol>
<VCol cols="12">
<AppTextField
id="invoice-subject"
v-model="invoiceSubject"
label="Subject"
placeholder="Invoice of purchased Admin Templates"
/>
</VCol>
<VCol cols="12">
<AppTextarea
id="payment-message"
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="tabler-link"
/>
Invoice Attached
</VChip>
</div>
<VBtn
type="submit"
class="me-3"
>
Send
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@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,237 @@
<script setup>
import {
animations,
remapNodes,
} from '@formkit/drag-and-drop'
import { dragAndDrop } from '@formkit/drag-and-drop/vue'
import { VForm } from 'vuetify/components/VForm'
import KanbanBoardEditDrawer from '@/views/apps/kanban/KanbanBoardEditDrawer.vue'
import KanbanItems from '@/views/apps/kanban/KanbanItems.vue'
const props = defineProps({
kanbanData: {
type: null,
required: true,
},
groupName: {
type: String,
required: false,
default: 'kanban',
},
})
const emit = defineEmits([
'addNewBoard',
'renameBoard',
'deleteBoard',
'addNewItem',
'editItem',
'deleteItem',
'updateItemsState',
'updateBoardState',
])
const kanbanWrapper = ref()
const localKanbanData = ref(props.kanbanData.boards)
const isKanbanBoardEditVisible = ref(false)
const isAddNewFormVisible = ref(false)
const refAddNewBoard = ref()
const boardTitle = ref('')
const editKanbanItem = ref()
// 👉 Add new board function that emit the name and id of new board
const addNewBoard = () => {
refAddNewBoard.value?.validate().then(valid => {
if (valid.valid) {
emit('addNewBoard', boardTitle.value)
isAddNewFormVisible.value = false
boardTitle.value = ''
}
})
}
const deleteBoard = boardId => {
emit('deleteBoard', boardId)
}
const renameBoard = boardName => {
emit('renameBoard', boardName)
}
const addNewItem = item => {
emit('addNewItem', item)
}
const editKanbanItemFn = item => {
if (item) {
editKanbanItem.value = item
isKanbanBoardEditVisible.value = true
}
}
const updateStateFn = kanbanState => {
emit('updateItemsState', kanbanState)
}
// 👉 initialize the drag and drop
dragAndDrop({
parent: kanbanWrapper,
values: localKanbanData,
dragHandle: '.drag-handler',
plugins: [animations()],
})
// assign the new kanban data to the local kanban data
watch(() => props, () => {
localKanbanData.value = props.kanbanData.boards
// 👉 remap the nodes when we rename the board: https://github.com/formkit/drag-and-drop/discussions/52#discussioncomment-8995203
remapNodes(kanbanWrapper.value)
}, { deep: true })
const emitUpdatedTaskFn = item => {
emit('editItem', item)
}
const deleteKanbanItemFn = item => {
emit('deleteItem', item)
}
// 👉 update boards data when it sort
watch(localKanbanData, () => {
const getIds = localKanbanData.value.map(board => board.id)
emit('updateBoardState', getIds)
}, { deep: true })
// 👉 validators for add new board
const validateBoardTitle = () => {
return props.kanbanData.boards.some(board => boardTitle.value && board.title.toLowerCase() === boardTitle.value.toLowerCase()) ? 'Board title already exists' : true
}
const hideAddNewForm = () => {
isAddNewFormVisible.value = false
refAddNewBoard.value?.reset()
}
// close add new item form when you loose focus from the form
onClickOutside(refAddNewBoard, hideAddNewForm)
</script>
<template>
<div class="kanban-main-wrapper d-flex gap-4 h-100">
<!-- 👉 kanban render -->
<div
ref="kanbanWrapper"
class="d-flex ga-6"
>
<template
v-for="kb in localKanbanData"
:key="kb.id"
>
<!-- 👉 kanban task render -->
<KanbanItems
:group-name="groupName"
:kanban-ids="kb.itemsIds"
:board-name="kb.title"
:board-id="kb.id"
:kanban-items="kanbanData.items"
:kanban-data="kanbanData"
@delete-board="deleteBoard"
@rename-board="renameBoard"
@add-new-item="addNewItem"
@edit-item="editKanbanItemFn"
@update-items-state="updateStateFn"
@delete-item="deleteKanbanItemFn"
/>
</template>
</div>
<!-- 👉 add new form -->
<div
class="add-new-form text-no-wrap"
style="inline-size: 10rem;"
>
<h6
class="text-lg font-weight-medium cursor-pointer"
@click="isAddNewFormVisible = !isAddNewFormVisible"
>
<VIcon
size="18"
icon="tabler-plus"
/> Add New
</h6>
<!-- 👉 Form -->
<VForm
v-if="isAddNewFormVisible"
ref="refAddNewBoard"
class="mt-4"
validate-on="submit"
@submit.prevent="addNewBoard"
>
<div class="mb-4">
<VTextField
v-model="boardTitle"
:rules="[requiredValidator, validateBoardTitle]"
autofocus
placeholder="Add Board Title"
@keydown.esc="hideAddNewForm"
/>
</div>
<div class="d-flex gap-3">
<VBtn
size="small"
type="submit"
>
Add
</VBtn>
<VBtn
size="small"
variant="tonal"
color="secondary"
type="reset"
@click="hideAddNewForm"
>
Cancel
</VBtn>
</div>
</VForm>
</div>
</div>
<!-- kanban edit drawer -->
<KanbanBoardEditDrawer
v-model:is-drawer-open="isKanbanBoardEditVisible"
:kanban-item="editKanbanItem"
@update:kanban-item="emitUpdatedTaskFn"
@delete-kanban-item="deleteKanbanItemFn"
/>
</template>
<style lang="scss">
@use "@styles/variables/_vuetify.scss" as vuetify;
.kanban-main-wrapper {
overflow: auto hidden;
margin-inline-start: -0.6rem;
min-block-size: calc(100vh - 10.5rem);
padding-inline-start: 0.6rem;
.kanban-board {
inline-size: 16.875rem;
min-inline-size: 16.875rem;
.kanban-board-drop-zone {
min-block-size: 100%;
}
}
.add-new-form {
.v-field__field {
border-radius: vuetify.$border-radius-root;
background-color: rgb(var(--v-theme-surface));
}
}
}
</style>

View File

@@ -0,0 +1,390 @@
<script setup>
import { Placeholder } from '@tiptap/extension-placeholder'
import { TextAlign } from '@tiptap/extension-text-align'
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'
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'
import avatar5 from '@images/avatars/avatar-5.png'
import avatar6 from '@images/avatars/avatar-6.png'
const props = defineProps({
kanbanItem: {
type: null,
required: false,
default: () => ({
item: {
title: '',
dueDate: '2022-01-01T00:00:00Z',
labels: [],
members: [],
id: 0,
attachments: 0,
commentsCount: 0,
image: '',
comments: '',
},
boardId: 0,
boardName: '',
}),
},
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDrawerOpen',
'update:kanbanItem',
'deleteKanbanItem',
])
const refEditTaskForm = ref()
const labelOptions = [
'UX',
'Image',
'Code Review',
'Dashboard',
'App',
'Charts & Maps',
]
const localKanbanItem = ref(JSON.parse(JSON.stringify(props.kanbanItem.item)))
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
if (!val)
refEditTaskForm.value?.reset()
}
// kanban item watcher
watch(() => props.kanbanItem, () => {
localKanbanItem.value = JSON.parse(JSON.stringify(props.kanbanItem.item))
}, { deep: true })
const updateKanbanItem = () => {
refEditTaskForm.value?.validate().then(async valid => {
if (valid.valid) {
emit('update:kanbanItem', {
item: localKanbanItem.value,
boardId: props.kanbanItem.boardId,
boardName: props.kanbanItem.boardName,
})
emit('update:isDrawerOpen', false)
await nextTick()
refEditTaskForm.value?.reset()
}
})
}
// delete kanban item
const deleteKanbanItem = () => {
emit('deleteKanbanItem', {
item: localKanbanItem.value,
boardId: props.kanbanItem.boardId,
boardName: props.kanbanItem.boardName,
})
emit('update:isDrawerOpen', false)
}
// 👉 label/chip color
const resolveLabelColor = {
'UX': 'success',
'Image': 'warning',
'Code Review': 'error',
'Dashboard': 'info',
'App': 'secondary',
'Charts & Maps': 'primary',
}
const users = [
{
img: avatar1,
name: 'John Doe',
},
{
img: avatar2,
name: 'Jane Smith',
},
{
img: avatar3,
name: 'Robert Johnson',
},
{
img: avatar4,
name: 'Lucy Brown',
},
{
img: avatar5,
name: 'Mike White',
},
{
img: avatar6,
name: 'Anna Black',
},
]
const fileAttached = ref()
const editor = useEditor({
content: '',
extensions: [
StarterKit,
TextAlign.configure({
types: [
'heading',
'paragraph',
],
}),
Placeholder.configure({ placeholder: 'Write a Comment...' }),
Underline,
],
})
const config = ref({
altFormat: 'j M, Y',
altInput: true,
dateFormat: 'Y-m-d',
})
</script>
<template>
<VNavigationDrawer
data-allow-mismatch
location="end"
:width="370"
temporary
border="0"
:model-value="props.isDrawerOpen"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Header -->
<AppDrawerHeaderSection
title="Edit Task"
@cancel="$emit('update:isDrawerOpen', false)"
/>
<VDivider />
<PerfectScrollbar
:options="{ wheelPropagation: false }"
style="block-size: calc(100vh - 4rem);"
>
<VForm
v-if="localKanbanItem"
ref="refEditTaskForm"
@submit.prevent="updateKanbanItem"
>
<VCardText class="kanban-editor-drawer">
<VRow>
<VCol cols="12">
<AppTextField
v-model="localKanbanItem.title"
label="Title"
:rules="[requiredValidator]"
/>
</VCol>
<VCol cols="12">
<AppDateTimePicker
v-model="localKanbanItem.dueDate"
label="Due date"
:config="config"
/>
</VCol>
<VCol cols="12">
<AppSelect
v-model="localKanbanItem.labels"
:items="labelOptions"
label="Label"
multiple
eager
>
<template #chip="{ item }">
<VChip :color="resolveLabelColor[item.raw]">
{{ item.raw }}
</VChip>
</template>
</AppSelect>
</VCol>
<VCol cols="12">
<p
class="mb-1 text-body-2 text-high-emphasis"
style="line-height: 15px;"
>
Assigned
</p>
<div>
<VSelect
v-model="localKanbanItem.members"
:items="users"
item-title="name"
item-value="name"
multiple
return-object
variant="plain"
:menu-props="{
offset: 10,
}"
class="assignee-select"
>
<template #selection="{ item }">
<VAvatar size="26">
<VImg :src="item.raw.img" />
<VTooltip activator="parent">
{{ item.raw.name }}
</VTooltip>
</VAvatar>
</template>
<template #prepend-inner>
<IconBtn
size="26"
variant="tonal"
color="secondary"
>
<VIcon
size="20"
icon="tabler-plus"
/>
</IconBtn>
</template>
</VSelect>
</div>
</VCol>
<VCol cols="12">
<VFileInput
v-model="fileAttached"
prepend-icon=""
multiple
variant="outlined"
label="No file chosen"
clearable
>
<template #append>
<VBtn variant="tonal">
Choose
</VBtn>
</template>
</VFileInput>
</VCol>
<VCol cols="12">
<p
class="text-body-2 text-high-emphasis mb-1"
style="line-height: 15px;"
>
COMMENT
</p>
<div class="border rounded px-3 py-2">
<EditorContent :editor="editor" />
<div
v-if="editor"
class="d-flex justify-end flex-wrap gap-x-2"
>
<VIcon
icon="tabler-bold"
:color="editor.isActive('bold') ? 'primary' : 'secondary'"
size="20"
@click="editor.chain().focus().toggleBold().run()"
/>
<VIcon
:color="editor.isActive('underline') ? 'primary' : 'secondary'"
icon="tabler-underline"
size="20"
@click="editor.commands.toggleUnderline()"
/>
<VIcon
:color="editor.isActive('italic') ? 'primary' : 'secondary'"
icon="tabler-italic"
size="20"
@click="editor.chain().focus().toggleItalic().run()"
/>
<VIcon
:color="editor.isActive({ textAlign: 'left' }) ? 'primary' : 'secondary'"
icon="tabler-align-left"
size="20"
@click="editor.chain().focus().setTextAlign('left').run()"
/>
<VIcon
:color="editor.isActive({ textAlign: 'center' }) ? 'primary' : 'secondary'"
icon="tabler-align-center"
size="20"
@click="editor.chain().focus().setTextAlign('center').run()"
/>
<VIcon
:color="editor.isActive({ textAlign: 'right' }) ? 'primary' : 'secondary'"
icon="tabler-align-right"
size="20"
@click="editor.chain().focus().setTextAlign('right').run()"
/>
</div>
</div>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
class="me-4"
>
Update
</VBtn>
<VBtn
color="error"
variant="tonal"
@click="deleteKanbanItem"
>
Delete
</VBtn>
</VCol>
</VRow>
</VCardText>
</VForm>
</PerfectScrollbar>
</VNavigationDrawer>
</template>
<style lang="scss">
.kanban-editor-drawer {
.assignee-select {
.v-field__append-inner {
.v-select__menu-icon {
display: none;
}
}
}
.ProseMirror {
padding: 0;
min-block-size: 7vh !important;
p {
margin-block-end: 0;
}
}
.ProseMirror-focused {
outline: none !important;
}
}
</style>

View File

@@ -0,0 +1,171 @@
<script setup>
const props = defineProps({
item: {
type: null,
required: true,
},
boardId: {
type: Number,
required: true,
},
boardName: {
type: String,
required: true,
},
})
const emit = defineEmits(['deleteKanbanItem'])
const resolveLabelColor = {
'UX': 'success',
'Image': 'warning',
'Code Review': 'error',
'Dashboard': 'info',
'App': 'secondary',
'Charts & Maps': 'primary',
}
const moreOptions = [
{
title: 'Copy Task link',
href: '#',
},
{
title: 'Duplicate Task',
href: '#',
},
{
title: 'Delete',
onClick: () => {
emit('deleteKanbanItem', {
item: props.item,
boardId: props.boardId,
boardName: props.boardName,
})
},
},
]
</script>
<template>
<VCard
v-if="item"
:ripple="false"
:link="false"
class="kanban-card position-relative"
>
<VCardText class="d-flex flex-column gap-2">
<div class="d-flex align-start gap-2">
<div
v-if="item.labels && item.labels.length"
class="d-flex flex-wrap gap-2"
>
<VChip
v-for="text in item.labels"
:key="text"
size="small"
:color="resolveLabelColor[text]"
>
{{ text }}
</VChip>
</div>
<VSpacer />
<VMenu>
<template #activator="{ props: p, isActive }">
<VIcon
v-bind="p"
icon="tabler-dots-vertical"
class="position-absolute more-options"
style="inset-block-start: 16px; inset-inline-end: 10px;"
:style="isActive ? 'opacity: 1' : ''"
size="20"
@click.stop
/>
</template>
<VList
:items="moreOptions"
item-props
/>
</VMenu>
</div>
<!-- Task Img -->
<VImg
v-if="item.image && item.image.length"
:src="item.image"
class="rounded"
/>
<!-- Task title -->
<p class="text-base text-high-emphasis mb-0">
{{ item.title }}
</p>
<!-- footer -->
<div class="task-footer d-flex align-center flex-wrap justify-space-between">
<div
v-if="item.attachments || item.commentsCount"
class="d-flex align-center gap-4"
>
<div v-if="item.attachments">
<VIcon
size="20"
icon="tabler-paperclip"
class="me-1"
/>
<span class="text-body-1 d-inline-block">{{ item.attachments }}</span>
</div>
<div v-if="item.commentsCount">
<VIcon
size="20"
icon="tabler-message-2"
class="me-1"
/>
<span class="text-body-1 d-inline-block">{{ item.commentsCount }}</span>
</div>
</div>
<div
v-if="item.members && item.members.length"
class="v-avatar-group"
>
<VAvatar
v-for="avatar in item.members"
:key="avatar.name"
size="30"
>
<VImg :src="avatar.img" />
<VTooltip activator="parent">
{{ avatar.name }}
</VTooltip>
</VAvatar>
</div>
</div>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.kanban-card {
cursor: grab;
:active {
cursor: grabbing;
}
&[style^="z-index"] {
cursor: grabbing !important;
}
.more-options {
opacity: 0;
}
&:hover .more-options {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,312 @@
<script setup>
import {
animations,
handleEnd,
performTransfer,
} from '@formkit/drag-and-drop'
import { dragAndDrop } from '@formkit/drag-and-drop/vue'
import { VForm } from 'vuetify/components/VForm'
import KanbanCard from '@/views/apps/kanban/KanbanCard.vue'
const props = defineProps({
kanbanIds: {
type: Array,
required: true,
},
groupName: {
type: String,
required: true,
},
boardName: {
type: String,
required: true,
},
boardId: {
type: Number,
required: true,
},
kanbanData: {
type: null,
required: true,
},
})
const emit = defineEmits([
'renameBoard',
'deleteBoard',
'addNewItem',
'editItem',
'updateItemsState',
'deleteItem',
])
const refKanbanBoard = ref()
const localBoardName = ref(props.boardName)
const localIds = ref(props.kanbanIds)
const isAddNewFormVisible = ref(false)
const isBoardNameEditing = ref(false)
const refForm = ref()
const newTaskTitle = ref('')
const refKanbanBoardTitle = ref()
// 👉 required validator
const boardActions = [
{
title: 'Rename',
prependIcon: 'tabler-pencil',
onClick: () => {
isBoardNameEditing.value = true
},
},
{
title: 'Delete',
prependIcon: 'tabler-trash',
onClick: () => emit('deleteBoard', props.boardId),
},
]
// 👉 emit rename board event
const renameBoard = () => {
refKanbanBoardTitle.value?.validate().then(valid => {
if (valid.valid) {
emit('renameBoard', {
oldName: props.boardName,
newName: localBoardName.value,
boardId: props.boardId,
})
isBoardNameEditing.value = false
}
})
}
// 👉 emit add new item event
const addNewItem = () => {
refForm.value?.validate().then(valid => {
if (valid.valid) {
emit('addNewItem', {
itemTitle: newTaskTitle.value,
boardName: props.boardName,
boardId: props.boardId,
})
isAddNewFormVisible.value = false
newTaskTitle.value = ''
}
})
}
// 👉 initialize draggable
dragAndDrop({
parent: refKanbanBoard,
values: localIds,
group: props.groupName,
draggable: child => child.classList.contains('kanban-card'),
plugins: [animations()],
performTransfer: (state, data) => {
performTransfer(state, data)
emit('updateItemsState', {
boardId: props.boardId,
ids: localIds.value,
})
},
handleEnd: data => {
handleEnd(data)
emit('updateItemsState', {
boardId: props.boardId,
ids: localIds.value,
})
},
})
// 👉 watch kanbanIds its is useful when you add new task
watch(() => props, () => {
localIds.value = props.kanbanIds
}, {
immediate: true,
deep: true,
})
const resolveItemUsingId = id => props.kanbanData.items.find(item => item.id === id)
const deleteItem = item => {
emit('deleteItem', item)
}
// 👉 reset add new item form when esc or close
const hideAddNewForm = () => {
isAddNewFormVisible.value = false
refForm.value?.reset()
}
// close add new item form when you loose focus from the form
onClickOutside(refForm, hideAddNewForm)
// close board name form when you loose focus from the form
onClickOutside(refKanbanBoardTitle, () => {
isBoardNameEditing.value = false
})
// 👉 reset board rename form when esc or close
const hideResetBoardNameForm = () => {
isBoardNameEditing.value = false
localBoardName.value = props.boardName
}
const handleEnterKeydown = event => {
if (event.key === 'Enter' && !event.shiftKey)
addNewItem()
}
</script>
<template>
<div class="kanban-board">
<!-- 👉 board heading and title -->
<div class="kanban-board-header pb-4">
<VForm
v-if="isBoardNameEditing"
ref="refKanbanBoardTitle"
@submit.prevent="renameBoard"
>
<VTextField
v-model="localBoardName"
autofocus
variant="underlined"
:rules="[requiredValidator]"
hide-details
class="border-0"
@keydown.esc="hideResetBoardNameForm"
>
<template #append-inner>
<VIcon
size="20"
color="success"
icon="tabler-check"
class="me-1"
@click="renameBoard"
/>
<VIcon
size="20"
color="error"
icon="tabler-x"
@click="hideResetBoardNameForm"
/>
</template>
</VTextField>
</VForm>
<div
v-else
class="d-flex align-center justify-space-between "
>
<h4 class="text-lg font-weight-medium text-truncate">
{{ boardName }}
</h4>
<div class="d-flex align-center">
<VIcon
class="drag-handler"
size="20"
icon="tabler-arrows-move"
/>
<MoreBtn
size="28"
icon-size="20"
class="text-high-emphasis"
:menu-list="boardActions"
item-props
/>
</div>
</div>
</div>
<!-- 👉 draggable task start here -->
<div
v-if="localIds"
ref="refKanbanBoard"
class="kanban-board-drop-zone rounded d-flex flex-column gap-4"
:class="localIds.length ? 'mb-4' : ''"
>
<template
v-for="id in localIds"
:key="id"
>
<KanbanCard
:item="resolveItemUsingId(id)"
:board-id="props.boardId"
:board-name="props.boardName"
@delete-kanban-item="deleteItem"
@click="emit('editItem', { item: resolveItemUsingId(id), boardId: props.boardId, boardName: props.boardName })"
/>
</template>
<!-- 👉 Add new Form -->
<div class="add-new-form">
<h6
class="text-base font-weight-regular cursor-pointer ms-4"
@click="isAddNewFormVisible = !isAddNewFormVisible"
>
<VIcon
size="15"
icon="tabler-plus"
/> Add New Item
</h6>
<VForm
v-if="isAddNewFormVisible"
ref="refForm"
class="mt-4"
validate-on="submit"
@submit.prevent="addNewItem"
>
<div class="mb-4">
<VTextarea
v-model="newTaskTitle"
:rules="[requiredValidator]"
placeholder="Add Content"
autofocus
rows="2"
@keydown.enter="handleEnterKeydown"
@keydown.esc="hideAddNewForm"
/>
</div>
<div class="d-flex gap-4 flex-wrap">
<VBtn
size="small"
type="submit"
>
Add
</VBtn>
<VBtn
size="small"
variant="tonal"
color="secondary"
@click="hideAddNewForm"
>
Cancel
</VBtn>
</div>
</VForm>
</div>
</div>
</div>
</template>
<style lang="scss">
.kanban-board-header {
.drag-handler {
cursor: grab;
opacity: 0;
&:active {
cursor: grabbing;
}
}
&:hover {
.drag-handler {
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,116 @@
<script setup>
const logisticData = ref([
{
icon: 'tabler-truck',
color: 'primary',
title: 'On route vehicles',
value: 42,
change: 18.2,
isHover: false,
},
{
icon: 'tabler-alert-triangle',
color: 'warning',
title: 'Vehicles with errors',
value: 8,
change: -8.7,
isHover: false,
},
{
icon: 'tabler-git-fork',
color: 'error',
title: 'Deviated from route',
value: 27,
change: 4.3,
isHover: false,
},
{
icon: 'tabler-clock',
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.38)`"
@mouseenter="data.isHover = true"
@mouseleave="data.isHover = false"
>
<VCardText>
<div class="d-flex align-center gap-x-4 mb-1">
<VAvatar
variant="tonal"
:color="data.color"
rounded
>
<VIcon
:icon="data.icon"
size="28"
/>
</VAvatar>
<h4 class="text-h4">
{{ data.value }}
</h4>
</div>
<div class="text-body-1 mb-1">
{{ data.title }}
</div>
<div class="d-flex gap-x-2 align-center">
<h6 class="text-h6">
{{ (data.change > 0) ? '+' : '' }} {{ data.change }}%
</h6>
<div class="text-sm text-disabled">
than last week
</div>
</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(8);
transition: all 0.1s ease-out;
}
}
.skin--bordered {
.logistics-card-statistics {
border-block-end-width: 2px;
&:hover {
border-block-end-width: 3px;
margin-block-end: -2px;
transition: all 0.1s ease-out;
}
}
}
</style>

View File

@@ -0,0 +1,116 @@
<script setup>
const chartColors = {
donut: {
series1: '#28C76F',
series2: '#28C76FCC',
series3: '#28C76F99',
series4: '#28C76F66',
},
}
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: '24px',
color: headingColor,
fontWeight: 500,
offsetY: -20,
formatter(val) {
return `${ Number.parseInt(val) }%`
},
},
name: { offsetY: 20 },
total: {
show: true,
fontSize: '0.9375rem',
fontWeight: 400,
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,112 @@
<script setup>
const deliveryData = [
{
title: 'Packages in transit',
value: '10k',
change: 25.8,
icon: 'tabler-box',
color: 'primary',
},
{
title: 'Packages out for delivery',
value: '5k',
change: 4.3,
icon: 'tabler-truck',
color: 'info',
},
{
title: 'Packages delivered',
value: '15k',
change: -12.5,
icon: 'tabler-circle-check',
color: 'success',
},
{
title: 'Delivery success rate',
value: '95%',
change: 35.6,
icon: 'tabler-percentage',
color: 'warning',
},
{
title: 'Average delivery time',
value: '2.5 Days',
change: -2.15,
icon: 'tabler-clock',
color: 'secondary',
},
{
title: 'Customer satisfaction',
value: '4.5/5',
change: 5.7,
icon: 'tabler-users',
color: 'error',
},
]
</script>
<template>
<VCard>
<VCardItem title="Delivery performance">
<VCardSubtitle>
12% increase in this month
</VCardSubtitle>
<template #append>
<MoreBtn />
</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="38"
class="me-1"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</template>
<VListItemTitle class="me-2">
{{ data.title }}
</VListItemTitle>
<VListItemSubtitle>
<div
:class="data.change > 0 ? 'text-success' : 'text-error'"
class="d-flex align-center gap-x-1"
>
<VIcon
:icon="data.change > 0 ? 'tabler-chevron-up' : 'tabler-chevron-down'"
size="20"
/>
<div>{{ data.change }}%</div>
</div>
</VListItemSubtitle>
<template #append>
<span class="text-body-1 font-weight-medium">
{{ data.value }}
</span>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,347 @@
<script setup>
const currentTab = ref('New')
const tabsData = [
'New',
'Preparing',
'Shipping',
]
</script>
<template>
<VCard class="country-order-card">
<VCardItem
title="Orders by countries"
subtitle="62 deliveries in progress"
>
<template #append>
<MoreBtn />
</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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="app-timeline-text">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="app-timeline-text">
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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="app-timeline-text">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="app-timeline-text">
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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="app-timeline-text">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="app-timeline-text">
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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="app-timeline-text">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="app-timeline-text">
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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Myrtle Ullrich
</div>
<div class="app-timeline-text">
101 Boulder, California(CA), 95959
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Barry Schowalter
</div>
<div class="app-timeline-text">
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="tabler-circle-check"
dot-color="rgba(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-uppercase text-success">
Sender
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="app-timeline-text">
162 Windsor, California(CA), 95492
</div>
</VTimelineItem>
<VTimelineItem
icon="tabler-map-pin"
dot-color="rgba(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-body-2 text-primary text-uppercase">
Receiver
</div>
<div class="app-timeline-title">
Helen Jacobs
</div>
<div class="app-timeline-text">
487 Sunset, California(CA), 94043
</div>
</VTimelineItem>
</VTimeline>
</div>
</VWindowItem>
</VWindow>
</VCardText>
</VCard>
</template>
<style lang="scss">
.country-order-card {
.v-timeline .v-timeline-divider__dot .v-timeline-divider__inner-dot {
box-shadow: none !important;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<script setup>
const itemsPerPage = ref(5)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const updateOptions = options => {
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>
<VDivider />
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
: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 }">
<VAvatar
variant="tonal"
color="secondary"
class="me-4"
size="40"
>
<VIcon
icon="tabler-car"
size="28"
/>
</VAvatar>
<RouterLink :to="{ name: 'apps-logistics-fleet' }">
<div class="text-link text-base font-weight-medium d-inline-block">
VOL-{{ item.location }}
</div>
</RouterLink>
</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)"
label
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>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalVehicles"
/>
</template>
</VDataTableServer>
</VCard>
</template>

View File

@@ -0,0 +1,241 @@
<script setup>
const chartColors = {
line: {
series1: '#FFB400',
series2: '#9055FD',
series3: '#7367f029',
},
}
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 borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
const series = [
{
name: 'Shipment',
type: 'column',
data: [
38,
45,
33,
38,
32,
48,
45,
40,
42,
37,
],
},
{
name: 'Delivery',
type: 'line',
data: [
23,
28,
23,
32,
25,
42,
32,
32,
26,
24,
],
},
]
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,
itemMargin: {
horizontal: 10,
vertical: 0,
},
fontSize: '15px',
fontFamily: 'Open Sans',
fontWeight: 400,
labels: {
colors: headingColor,
useSeriesColors: !1,
},
offsetY: 10,
},
grid: {
strokeDashArray: 8,
borderColor,
},
colors: [
chartColors.line.series1,
chartColors.line.series2,
],
fill: {
opacity: [
1,
1,
],
},
plotOptions: {
bar: {
columnWidth: '30%',
borderRadius: 4,
borderRadiusApplication: 'end',
},
},
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',
fontWeight: 400,
},
},
axisBorder: { show: false },
axisTicks: { show: false },
},
yaxis: {
tickAmount: 4,
min: 0,
max: 50,
labels: {
style: {
colors: labelColor,
fontSize: '13px',
fontWeight: 400,
},
formatter(val) {
return `${ val }%`
},
},
},
responsive: [
{
breakpoint: 1400,
options: {
chart: { height: 320 },
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>
<VBtn
variant="tonal"
append-icon="tabler-chevron-down"
>
January
</VBtn>
</template>
</VCardItem>
<VCardText>
<VueApexCharts
id="shipment-statistics"
type="line"
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: 16px !important;
}
.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 12px;
}
}
</style>

View File

@@ -0,0 +1,188 @@
<script setup>
const vehicleData = [
{
icon: 'tabler-car',
title: 'On the way',
time: '2hr 10min',
percentage: 39.7,
},
{
icon: 'tabler-circle-arrow-down',
title: 'Unloading',
time: '3hr 15min',
percentage: 28.3,
},
{
icon: 'tabler-circle-arrow-up',
title: 'Loading',
time: '1hr 24min',
percentage: 17.4,
},
{
icon: 'tabler-clock',
title: 'Waiting',
time: '5hr 19min',
percentage: 14.6,
},
]
</script>
<template>
<VCard>
<VCardItem title="Vehicles Overview">
<template #append>
<MoreBtn />
</template>
</VCardItem>
<VCardText>
<div class="d-flex mb-6">
<div style="inline-size: 39.7%;">
<div class="vehicle-progress-label position-relative mb-6 text-body-1 d-none d-sm-block">
On the way
</div>
<VProgressLinear
color="rgba(var(--v-theme-on-surface), var(--v-hover-opacity))"
model-value="100"
height="46"
class="rounded-e-0 rounded-lg"
>
<div class="text-start text-sm font-weight-medium">
39.7%
</div>
</VProgressLinear>
</div>
<div style="inline-size: 28.3%;">
<div class="vehicle-progress-label position-relative mb-6 text-body-1 d-none d-sm-block">
Unloading
</div>
<VProgressLinear
color="rgb(var(--v-theme-primary))"
model-value="100"
class="rounded-0"
height="46"
>
<div class="text-white text-sm font-weight-medium text-start">
28.3%
</div>
</VProgressLinear>
</div>
<div style="inline-size: 17.4%;">
<div class="vehicle-progress-label position-relative mb-6 text-body-1 d-none d-sm-block">
Loading
</div>
<VProgressLinear
color="rgb(var(--v-theme-info))"
model-value="100"
height="46"
class="rounded-0"
>
<div class="text-white text-sm font-weight-medium text-start">
17.4%
</div>
</VProgressLinear>
</div>
<div style="inline-size: 14.6%;">
<div class="vehicle-progress-label position-relative mb-6 text-body-1 d-none d-sm-block">
Waiting
</div>
<VProgressLinear
color="rgb(var(--v-tooltip-background))"
model-value="100"
height="46"
class="rounded-s-0 rounded-lg"
>
<div class="text-sm text-surface font-weight-medium text-start">
14.6%
</div>
</VProgressLinear>
</div>
</div>
<VTable class="text-no-wrap">
<tbody>
<tr
v-for="(vehicle, index) in vehicleData"
:key="index"
>
<td
width="70%"
style="padding-inline-start: 0 !important;"
>
<div class="d-flex align-center gap-x-2">
<VIcon
:icon="vehicle.icon"
size="24"
class="text-high-emphasis"
/>
<div class="text-body-1 text-high-emphasis">
{{ vehicle.title }}
</div>
</div>
</td>
<td>
<h6 class="text-h6">
{{ vehicle.time }}
</h6>
</td>
<td>
<div class="text-body-1">
{{ vehicle.percentage }}%
</div>
</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;
}
#shipment-statistics .apexcharts-legend-series {
padding-inline: 16px;
}
@media (max-width: 1080px) {
#shipment-statistics .apexcharts-legend-series {
padding-inline: 12px;
}
.v-progress-linear__content {
padding-inline-start: 0.75rem !important;
}
}
@media (max-width: 576px) {
#shipment-statistics .apexcharts-legend-series {
padding-inline: 8px;
}
.v-progress-linear__content {
padding-inline-start: 0.125rem !important;
}
}
</style>

View File

@@ -0,0 +1,330 @@
<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 girlUsingMobile from '@images/pages/girl-using-mobile.png'
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 pb-4">
<div class="text-body-1">
Total {{ item.users.length }} users
</div>
<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.current.dark ? '#373B50' : '#EEEDF0'"
>
<span>
+{{ item.users.length - 3 }}
</span>
</VAvatar>
</div>
</VCardText>
<VCardText>
<div class="d-flex justify-space-between align-center">
<div>
<h5 class="text-h5">
{{ item.role }}
</h5>
<div class="d-flex align-center">
<a
href="javascript:void(0)"
@click="editPermission(item.details)"
>
Edit Role
</a>
</div>
</div>
<IconBtn>
<VIcon
icon="tabler-copy"
class="text-high-emphasis"
/>
</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="85"
:src="girlUsingMobile"
>
</VCol>
<VCol cols="7">
<VCardText class="d-flex flex-column align-end justify-end gap-4">
<VBtn
size="small"
@click="isAddRoleDialogVisible = true"
>
Add New Role
</VBtn>
<div class="text-end">
Add new role,<br> if it doesn't exist.
</div>
</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,364 @@
<script setup>
import AddNewUserDrawer from '@/views/apps/user/list/AddNewUserDrawer.vue'
// 👉 Store
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 selectedRows = ref([])
const updateOptions = options => {
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
// Headers
const headers = [
{
title: 'User',
key: 'user',
},
{
title: 'Role',
key: 'role',
},
{
title: 'Plan',
key: 'plan',
},
{
title: 'Billing',
key: 'billing',
},
{
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: 'primary',
icon: 'tabler-user',
}
if (roleLowerCase === 'author')
return {
color: 'warning',
icon: 'tabler-settings',
}
if (roleLowerCase === 'maintainer')
return {
color: 'success',
icon: 'tabler-chart-donut',
}
if (roleLowerCase === 'editor')
return {
color: 'info',
icon: 'tabler-pencil',
}
if (roleLowerCase === 'admin')
return {
color: 'error',
icon: 'tabler-device-laptop',
}
return {
color: 'primary',
icon: 'tabler-user',
}
}
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 isAddNewUserDrawerVisible = ref(false)
const addNewUser = async userData => {
await $api('/apps/users', {
method: 'POST',
body: userData,
})
// refetch User
fetchUsers()
}
const deleteUser = async id => {
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
// Delete from selectedRows
const index = selectedRows.value.findIndex(row => row === id)
if (index !== -1)
selectedRows.value.splice(index, 1)
// refetch User
fetchUsers()
}
</script>
<template>
<section>
<VCard>
<VCardText class="d-flex flex-wrap gap-4">
<div class="d-flex gap-2 align-center">
<p class="text-body-1 mb-0">
Show
</p>
<AppSelect
:model-value="itemsPerPage"
:items="[
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' },
{ value: 100, title: '100' },
{ value: -1, title: 'All' },
]"
style="inline-size: 5.5rem;"
@update:model-value="itemsPerPage = parseInt($event, 10)"
/>
</div>
<VSpacer />
<div class="d-flex align-center flex-wrap gap-4">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
placeholder="Search User"
style="inline-size: 15.625rem;"
/>
<!-- 👉 Add user button -->
<AppSelect
v-model="selectedRole"
placeholder="Select Role"
:items="roles"
clearable
clear-icon="tabler-x"
style="inline-size: 10rem;"
/>
</div>
</VCardText>
<VDivider />
<!-- SECTION datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:model-value="selectedRows"
v-model:page="page"
:items-per-page-options="[
{ value: 10, title: '10' },
{ value: 20, title: '20' },
{ value: 50, title: '50' },
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
]"
:items="users"
:items-length="totalUsers"
:headers="headers"
class="text-no-wrap"
show-select
@update:options="updateOptions"
>
<!-- User -->
<template #item.user="{ item }">
<div class="d-flex align-center gap-x-4">
<VAvatar
size="34"
:variant="!item.avatar ? 'tonal' : undefined"
:color="!item.avatar ? resolveUserRoleVariant(item.role).color : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column">
<h6 class="text-base">
<RouterLink
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
class="font-weight-medium text-link"
>
{{ item.fullName }}
</RouterLink>
</h6>
<div class="text-sm">
{{ item.email }}
</div>
</div>
</div>
</template>
<!-- 👉 Role -->
<template #item.role="{ item }">
<div class="d-flex align-center gap-x-2">
<VIcon
:size="22"
:icon="resolveUserRoleVariant(item.role).icon"
:color="resolveUserRoleVariant(item.role).color"
/>
<div class="text-capitalize text-high-emphasis text-body-1">
{{ item.role }}
</div>
</div>
</template>
<!-- Plan -->
<template #item.plan="{ item }">
<div class="text-body-1 text-high-emphasis text-capitalize">
{{ item.currentPlan }}
</div>
</template>
<!-- Status -->
<template #item.status="{ item }">
<VChip
:color="resolveUserStatusVariant(item.status)"
size="small"
label
class="text-capitalize"
>
{{ item.status }}
</VChip>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn @click="deleteUser(item.id)">
<VIcon icon="tabler-trash" />
</IconBtn>
<IconBtn>
<VIcon icon="tabler-eye" />
</IconBtn>
<VBtn
icon
variant="text"
color="medium-emphasis"
>
<VIcon icon="tabler-dots-vertical" />
<VMenu activator="parent">
<VList>
<VListItem :to="{ name: 'apps-user-view-id', params: { id: item.id } }">
<template #prepend>
<VIcon icon="tabler-eye" />
</template>
<VListItemTitle>View</VListItemTitle>
</VListItem>
<VListItem link>
<template #prepend>
<VIcon icon="tabler-pencil" />
</template>
<VListItemTitle>Edit</VListItemTitle>
</VListItem>
<VListItem @click="deleteUser(item.id)">
<template #prepend>
<VIcon icon="tabler-trash" />
</template>
<VListItemTitle>Delete</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</template>
<template #bottom>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalUsers"
/>
</template>
</VDataTableServer>
<!-- SECTION -->
</VCard>
<!-- 👉 Add New User -->
<AddNewUserDrawer
v-model:is-drawer-open="isAddNewUserDrawerVisible"
@user-data="addNewUser"
/>
</section>
</template>
<style lang="scss">
.text-capitalize {
text-transform: capitalize;
}
.user-list-name:not(:hover) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
</style>

View File

@@ -0,0 +1,213 @@
<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,
country: country.value,
contact: contact.value,
email: email.value,
currentPlan: plan.value,
status: status.value,
avatar: '',
billing: 'Auto Debit',
})
emit('update:isDrawerOpen', false)
nextTick(() => {
refForm.value?.reset()
refForm.value?.resetValidation()
})
}
})
}
const handleDrawerModelValueUpdate = val => {
emit('update:isDrawerOpen', val)
}
</script>
<template>
<VNavigationDrawer
data-allow-mismatch
temporary
:width="400"
location="end"
class="scrollable-content"
:model-value="props.isDrawerOpen"
@update:model-value="handleDrawerModelValueUpdate"
>
<!-- 👉 Title -->
<AppDrawerHeaderSection
title="Add New 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">
<AppTextField
v-model="fullName"
:rules="[requiredValidator]"
label="Full Name"
placeholder="John Doe"
/>
</VCol>
<!-- 👉 Username -->
<VCol cols="12">
<AppTextField
v-model="userName"
:rules="[requiredValidator]"
label="Username"
placeholder="Johndoe"
/>
</VCol>
<!-- 👉 Email -->
<VCol cols="12">
<AppTextField
v-model="email"
:rules="[requiredValidator, emailValidator]"
label="Email"
placeholder="johndoe@email.com"
/>
</VCol>
<!-- 👉 company -->
<VCol cols="12">
<AppTextField
v-model="company"
:rules="[requiredValidator]"
label="Company"
placeholder="PixInvent"
/>
</VCol>
<!-- 👉 Country -->
<VCol cols="12">
<AppSelect
v-model="country"
label="Select Country"
placeholder="Select Country"
:rules="[requiredValidator]"
:items="['USA', 'UK', 'India', 'Australia']"
/>
</VCol>
<!-- 👉 Contact -->
<VCol cols="12">
<AppTextField
v-model="contact"
type="number"
:rules="[requiredValidator]"
label="Contact"
placeholder="+1-541-754-3010"
/>
</VCol>
<!-- 👉 Role -->
<VCol cols="12">
<AppSelect
v-model="role"
label="Select Role"
placeholder="Select Role"
:rules="[requiredValidator]"
:items="['Admin', 'Author', 'Editor', 'Maintainer', 'Subscriber']"
/>
</VCol>
<!-- 👉 Plan -->
<VCol cols="12">
<AppSelect
v-model="plan"
label="Select Plan"
placeholder="Select Plan"
:rules="[requiredValidator]"
:items="['Basic', 'Company', 'Enterprise', 'Team']"
/>
</VCol>
<!-- 👉 Status -->
<VCol cols="12">
<AppSelect
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-3"
>
Submit
</VBtn>
<VBtn
type="reset"
variant="tonal"
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,367 @@
<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 resolveUserRoleVariant = role => {
if (role === 'subscriber')
return {
color: 'warning',
icon: 'tabler-user',
}
if (role === 'author')
return {
color: 'success',
icon: 'tabler-circle-check',
}
if (role === 'maintainer')
return {
color: 'primary',
icon: 'tabler-chart-pie-2',
}
if (role === 'editor')
return {
color: 'info',
icon: 'tabler-pencil',
}
if (role === 'admin')
return {
color: 'secondary',
icon: 'tabler-server-2',
}
return {
color: 'primary',
icon: 'tabler-user',
}
}
</script>
<template>
<VRow>
<!-- SECTION User Details -->
<VCol cols="12">
<VCard v-if="props.userData">
<VCardText class="text-center pt-12">
<!-- 👉 Avatar -->
<VAvatar
rounded
:size="100"
: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
label
:color="resolveUserRoleVariant(props.userData.role).color"
size="small"
class="text-capitalize mt-4"
>
{{ props.userData.role }}
</VChip>
</VCardText>
<VCardText>
<div class="d-flex justify-space-around gap-x-6 gap-y-2 flex-wrap mb-6">
<!-- 👉 Done task -->
<div class="d-flex align-center me-8">
<VAvatar
:size="40"
rounded
color="primary"
variant="tonal"
class="me-4"
>
<VIcon
icon="tabler-checkbox"
size="24"
/>
</VAvatar>
<div>
<h5 class="text-h5">
{{ `${(props.userData.taskDone / 1000).toFixed(2)}k` }}
</h5>
<span class="text-sm">Task Done</span>
</div>
</div>
<!-- 👉 Done Project -->
<div class="d-flex align-center me-4">
<VAvatar
:size="38"
rounded
color="primary"
variant="tonal"
class="me-4"
>
<VIcon
icon="tabler-briefcase"
size="24"
/>
</VAvatar>
<div>
<h5 class="text-h5">
{{ kFormatter(props.userData.projectDone) }}
</h5>
<span class="text-sm">Project Done</span>
</div>
</div>
</div>
<!-- 👉 Details -->
<h5 class="text-h5">
Details
</h5>
<VDivider class="my-4" />
<!-- 👉 User Details list -->
<VList class="card-list mt-2">
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Username:
<div class="d-inline-block text-body-1">
{{ props.userData.fullName }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<span class="text-h6">
Billing Email:
</span>
<span class="text-body-1">
{{ props.userData.email }}
</span>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Status:
<div class="d-inline-block text-body-1 text-capitalize">
{{ props.userData.status }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Role:
<div class="d-inline-block text-capitalize text-body-1">
{{ props.userData.role }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Tax ID:
<div class="d-inline-block text-body-1">
{{ props.userData.taxId }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Contact:
<div class="d-inline-block text-body-1">
{{ props.userData.contact }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Language:
<div class="d-inline-block text-body-1">
{{ props.userData.language }}
</div>
</h6>
</VListItemTitle>
</VListItem>
<VListItem>
<VListItemTitle>
<h6 class="text-h6">
Country:
<div class="d-inline-block text-body-1">
{{ props.userData.country }}
</div>
</h6>
</VListItemTitle>
</VListItem>
</VList>
</VCardText>
<!-- 👉 Edit and Suspend button -->
<VCardText class="d-flex justify-center gap-x-4">
<VBtn
variant="elevated"
@click="isUserInfoEditDialogVisible = true"
>
Edit
</VBtn>
<VBtn
variant="tonal"
color="error"
>
Suspend
</VBtn>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
<!-- SECTION Current Plan -->
<VCol cols="12">
<VCard>
<VCardText class="d-flex">
<!-- 👉 Standard Chip -->
<VChip
label
color="primary"
size="small"
class="font-weight-medium"
>
Popular
</VChip>
<VSpacer />
<!-- 👉 Current Price -->
<div class="d-flex align-center">
<sup class="text-h5 text-primary mt-1">$</sup>
<h1 class="text-h1 text-primary">
99
</h1>
<sub class="mt-3"><h6 class="text-h6 font-weight-regular mb-n1">/ 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 gap-x-2">
<VIcon
size="10"
color="secondary"
icon="tabler-circle-filled"
/>
<div class="text-medium-emphasis">
{{ benefit }}
</div>
</div>
</VListItem>
</VList>
<!-- 👉 Days -->
<div class="my-6">
<div class="d-flex justify-space-between mb-1">
<h6 class="text-h6">
Days
</h6>
<h6 class="text-h6">
26 of 30 Days
</h6>
</div>
<!-- 👉 Progress -->
<VProgressLinear
rounded
rounded-bar
:model-value="65"
color="primary"
/>
<p class="mt-1">
4 days remaining
</p>
</div>
<!-- 👉 Upgrade Plan -->
<div class="d-flex gap-4">
<VBtn
block
@click="isUpgradePlanDialogVisible = true"
>
Upgrade Plan
</VBtn>
</div>
</VCardText>
</VCard>
</VCol>
<!-- !SECTION -->
</VRow>
<!-- 👉 Edit user info dialog -->
<UserInfoEditDialog
v-model:is-dialog-visible="isUserInfoEditDialogVisible"
:user-data="props.userData"
/>
<!-- 👉 Upgrade plan dialog -->
<UserUpgradePlanDialog v-model:is-dialog-visible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 0.5rem;
}
.text-capitalize {
text-transform: capitalize !important;
}
</style>

View File

@@ -0,0 +1,261 @@
<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 => {
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const isLoading = ref(false)
// 👉 headers
const headers = [
{
title: '#',
key: 'id',
},
{
title: 'Status',
key: 'trending',
sortable: false,
},
{
title: 'Total',
key: 'total',
},
{
title: 'Issued Date',
key: 'date',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
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)
const resolveInvoiceStatusVariantAndIcon = status => {
if (status === 'Partial Payment')
return {
variant: 'success',
icon: 'tabler-check',
}
if (status === 'Paid')
return {
variant: 'warning',
icon: 'tabler-chart-pie',
}
if (status === 'Downloaded')
return {
variant: 'info',
icon: 'tabler-arrow-down',
}
if (status === 'Draft')
return {
variant: 'primary',
icon: 'tabler-folder',
}
if (status === 'Sent')
return {
variant: 'secondary',
icon: 'tabler-mail',
}
if (status === 'Past Due')
return {
variant: 'error',
icon: 'tabler-info-circle',
}
return {
variant: 'secondary',
icon: 'tabler-x',
}
}
const computedMoreList = computed(() => {
return paramId => [
{
title: 'Download',
value: 'download',
prependIcon: 'tabler-download',
},
{
title: 'Edit',
value: 'edit',
prependIcon: 'tabler-pencil',
to: {
name: 'apps-invoice-edit-id',
params: { id: paramId },
},
},
{
title: 'Duplicate',
value: 'duplicate',
prependIcon: 'tabler-layers-intersect',
},
]
})
const deleteInvoice = async id => {
await $api(`/apps/invoice/${ id }`, { method: 'DELETE' })
fetchInvoices()
}
</script>
<template>
<section v-if="invoices">
<VCard id="invoice-list">
<VCardText>
<div class="d-flex align-center justify-space-between flex-wrap gap-4">
<div class="text-h5">
Invoice List
</div>
<div class="d-flex align-center gap-x-4">
<AppSelect
:model-value="itemsPerPage"
:items="[
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' },
{ value: 100, title: '100' },
{ value: -1, title: 'All' },
]"
style="inline-size: 6.25rem;"
@update:model-value="itemsPerPage = parseInt($event, 10)"
/>
<!-- 👉 Export invoice -->
<VBtn
append-icon="tabler-upload"
variant="tonal"
color="secondary"
>
Export
</VBtn>
</div>
</div>
</VCardText>
<VDivider />
<!-- SECTION Datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:loading="isLoading"
:items-length="totalInvoices"
:headers="headers"
:items="invoices"
item-value="total"
class="text-no-wrap text-sm rounded-0"
@update:options="updateOptions"
>
<!-- 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>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn @click="deleteInvoice(item.id)">
<VIcon icon="tabler-trash" />
</IconBtn>
<IconBtn :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
<VIcon icon="tabler-eye" />
</IconBtn>
<MoreBtn
:menu-list="computedMoreList(item.id)"
color="undefined"
item-props
/>
</template>
<template #bottom>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalInvoices"
/>
</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,424 @@
<script setup>
import UserInvoiceTable from './UserInvoiceTable.vue'
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'
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 figma from '@images/icons/project-icons/figma.png'
import html5 from '@images/icons/project-icons/html5.png'
import pdf from '@images/icons/project-icons/pdf.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: 'project',
},
{
title: 'LEADER',
key: 'leader',
},
{
title: 'Team',
key: 'team',
},
{
title: 'PROGRESS',
key: 'progress',
},
{
title: 'Action',
key: 'Action',
sortable: false,
},
]
const search = ref('')
const options = ref({
itemsPerPage: 5,
page: 1,
})
const projects = [
{
logo: react,
name: 'BGC eCommerce App',
project: 'React Project',
leader: 'Eileen',
progress: 78,
hours: '18:42',
team: [
avatar1,
avatar8,
avatar6,
],
extraMembers: 3,
},
{
logo: figma,
name: 'Falcon Logo Design',
project: 'Figma Project',
leader: 'Owen',
progress: 25,
hours: '20:42',
team: [
avatar5,
avatar2,
],
},
{
logo: vue,
name: 'Dashboard Design',
project: 'Vuejs Project',
leader: 'Keith',
progress: 62,
hours: '120:87',
team: [
avatar8,
avatar2,
avatar1,
],
},
{
logo: xamarin,
name: 'Foodista mobile app',
project: 'Xamarin Project',
leader: 'Merline',
progress: 8,
hours: '120:87',
team: [
avatar3,
avatar4,
avatar7,
],
extraMembers: 8,
},
{
logo: python,
name: 'Dojo Email App',
project: 'Python Project',
leader: 'Harmonia',
progress: 51,
hours: '230:10',
team: [
avatar4,
avatar3,
avatar1,
],
extraMembers: 5,
},
{
logo: sketch,
name: 'Blockchain Website',
project: 'Sketch Project',
leader: 'Allyson',
progress: 92,
hours: '342:41',
team: [
avatar1,
avatar8,
],
},
{
logo: html5,
name: 'Hoffman Website',
project: 'HTML Project',
leader: 'Georgie',
progress: 80,
hours: '12:45',
team: [
avatar1,
avatar8,
avatar6,
],
},
]
const moreList = [
{
title: 'Download',
value: 'Download',
},
{
title: 'Delete',
value: 'Delete',
},
{
title: 'View',
value: 'View',
},
]
</script>
<template>
<VRow>
<VCol cols="12">
<VCard>
<VCardText class="d-flex justify-space-between align-center flex-wrap gap-4">
<h5 class="text-h5">
User's Projects List
</h5>
<div style="inline-size: 250px;">
<AppTextField
v-model="search"
placeholder="Search Project"
/>
</div>
</VCardText>
<VDivider />
<!-- 👉 User Project List Table -->
<!-- SECTION Datatable -->
<VDataTable
v-model:page="options.page"
:headers="projectTableHeaders"
:items-per-page="options.itemsPerPage"
:items="projects"
item-value="name"
hide-default-footer
:search="search"
show-select
class="text-no-wrap"
>
<!-- projects -->
<template #item.project="{ item }">
<div class="d-flex align-center gap-x-3">
<VAvatar
:size="34"
:image="item.logo"
/>
<div>
<h6 class="text-h6 text-no-wrap">
{{ item.name }}
</h6>
<div class="text-body-2">
{{ item.project }}
</div>
</div>
</div>
</template>
<template #item.leader="{ item }">
<div class="text-base text-high-emphasis">
{{ item.leader }}
</div>
</template>
<!-- Team -->
<template #item.team="{ item }">
<div class="d-flex">
<div class="v-avatar-group">
<VAvatar
v-for="(data, index) in item.team"
:key="index"
size="26"
>
<VImg :src="data" />
</VAvatar>
<VAvatar
v-if="item.extraMembers"
:color="$vuetify.theme.current.dark ? '#373b50' : '#eeedf0'"
:size="26"
>
<div class="text-caption text-high-emphasis">
+{{ item.extraMembers }}
</div>
</VAvatar>
</div>
</div>
</template>
<!-- Progress -->
<template #item.progress="{ item }">
<div class="d-flex align-center gap-3">
<div class="flex-grow-1">
<VProgressLinear
:height="6"
:model-value="item.progress"
color="primary"
rounded
/>
</div>
<div class="text-body-1 text-high-emphasis">
{{ item.progress }}%
</div>
</div>
</template>
<!-- Action -->
<template #item.Action>
<MoreBtn :menu-list="moreList" />
</template>
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
<template #bottom>
<TablePagination
v-model:page="options.page"
:items-per-page="options.itemsPerPage"
:total-items="projects.length"
/>
</template>
</VDataTable>
<!-- !SECTION -->
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 User Activity timeline -->
<VCard title="User Activity Timeline">
<VCardText>
<VTimeline
side="end"
align="start"
line-inset="8"
truncate-line="start"
density="compact"
>
<!-- SECTION Timeline Item: Flight -->
<VTimelineItem
dot-color="primary"
size="x-small"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center gap-2 flex-wrap mb-2">
<span class="app-timeline-title">
12 Invoices have been paid
</span>
<span class="app-timeline-meta">12 min ago</span>
</div>
<!-- 👉 Content -->
<div class="app-timeline-text mt-1">
Invoices have been paid to the company
</div>
<div class="d-inline-flex align-center timeline-chip mt-2">
<img
:src="pdf"
height="20"
class="me-2"
alt="img"
>
<span class="app-timeline-text font-weight-medium">
invoice.pdf
</span>
</div>
</VTimelineItem>
<!-- !SECTION -->
<!-- SECTION Timeline Item: Interview Schedule -->
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap mb-2">
<div class="app-timeline-title">
Client Meeting
</div>
<span class="app-timeline-meta">45 min ago</span>
</div>
<div class="app-timeline-text mt-1">
Project meeting with john @10:15am
</div>
<!-- 👉 Person -->
<div class="d-flex justify-space-between align-center flex-wrap">
<!-- 👉 Avatar & Personal Info -->
<div class="d-flex align-center mt-2">
<VAvatar
size="32"
class="me-2"
:image="avatar1"
/>
<div class="d-flex flex-column">
<p class="text-sm font-weight-medium text-medium-emphasis mb-0">
Lester McCarthy (Client)
</p>
<span class="text-sm">CEO of Pixinvent</span>
</div>
</div>
</div>
</VTimelineItem>
<!-- !SECTION -->
<!-- SECTION Design Review -->
<VTimelineItem
size="x-small"
dot-color="info"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap mb-2">
<span class="app-timeline-title">
Create a new project for client
</span>
<span class="app-timeline-meta">2 Day Ago</span>
</div>
<!-- 👉 Content -->
<p class="app-timeline-text mt-1 mb-2">
6 team members in a project
</p>
<div class="v-avatar-group demo-avatar-group">
<VAvatar :size="40">
<VImg :src="avatar1" />
<VTooltip
activator="parent"
location="top"
>
John Doe
</VTooltip>
</VAvatar>
<VAvatar :size="40">
<VImg :src="avatar2" />
<VTooltip
activator="parent"
location="top"
>
Jennie Obrien
</VTooltip>
</VAvatar>
<VAvatar :size="40">
<VImg :src="avatar3" />
<VTooltip
activator="parent"
location="top"
>
Peter Harper
</VTooltip>
</VAvatar>
<VAvatar
:size="40"
:color="$vuetify.theme.current.dark ? '#373b50' : '#eeedf0'"
>
+3
</VAvatar>
</div>
</VTimelineItem>
<!-- !SECTION -->
</VTimeline>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<UserInvoiceTable />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,425 @@
<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,
isExpired: false,
type: 'mastercard',
cvv: '123',
image: mastercard,
},
{
name: 'Mildred Wagner',
number: '5531234567895678',
expiry: '02/24',
isPrimary: false,
isExpired: false,
type: 'visa',
cvv: '456',
image: visa,
},
{
name: 'Lester Jennings',
number: '5531234567890002',
expiry: '08/20',
isPrimary: false,
isExpired: true,
type: 'visa',
cvv: '456',
image: americanExpress,
},
]
const currentAddress = {
companyName: 'Pixinvent',
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 currentBillingAddress = {
firstName: 'Shamus',
lastName: 'Tuttle',
selectedCountry: 'USA',
addressLine1: '45 Rocker Terrace',
addressLine2: 'Latheronwheel',
landmark: 'KW5 8NW, London',
contact: '+1 (609) 972-22-22',
country: 'USA',
city: 'London',
state: 'London',
zipCode: 110001,
}
</script>
<template>
<VRow>
<!-- 👉 Current Plan -->
<VCol cols="12">
<VCard title="Current Plan">
<VCardText>
<VRow>
<VCol
cols="12"
md="6"
order-md="1"
order="2"
>
<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="d-inline-block me-2">$99 Per Month</span>
<VChip
color="primary"
size="small"
label
>
Popular
</VChip>
</h6>
<p class="mb-0">
Standard plan for small to medium businesses
</p>
</VCol>
<VCol
cols="12"
md="6"
order-md="2"
order="1"
>
<!-- 👉 Alert -->
<VAlert
color="warning"
variant="tonal"
>
<VAlertTitle class="mb-1">
We need your attention!
</VAlertTitle>
<div class="text-base">
Your plan requires update
</div>
</VAlert>
<!-- 👉 Progress -->
<div class="d-flex justify-space-between font-weight-bold mt-4 mb-2">
<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"
order="3"
>
<div class="d-flex flex-wrap gap-4">
<VBtn @click="isUpgradePlanDialogVisible = true">
upgrade plan
</VBtn>
<VBtn
color="error"
variant="tonal"
>
Cancel Subscription
</VBtn>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Payment Methods -->
<VCol cols="12">
<VCard title="Payment Methods">
<template #append>
<VBtn
prepend-icon="tabler-plus"
size="small"
@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 gap-6 justify-space-between">
<div class="text-no-wrap">
<img
:src="card.image"
:height="25"
>
<div class="my-2 d-flex gap-x-2 align-center">
<h6 class="text-h6">
{{ card.name }}
</h6>
<VChip
v-if="card.isPrimary || card.isExpired"
label
:color="card.isPrimary ? 'primary' : card.isExpired ? 'error' : 'secondary'"
size="small"
>
{{ card.isPrimary ? 'Popular' : card.isExpired ? 'Expired' : '' }}
</VChip>
</div>
<div class="text-body-1">
**** **** **** {{ card.number.substring(card.number.length - 4) }}
</div>
</div>
<div class="d-flex flex-column text-sm-end gap-y-4">
<div class="order-sm-0 order-1">
<VBtn
variant="tonal"
size="small"
class="me-4"
@click="openEditCardDialog(card)"
>
Edit
</VBtn>
<VBtn
color="error"
variant="tonal"
size="small"
>
Delete
</VBtn>
</div>
<div class="order-sm-1 order-0 text-sm">
Card expires at {{ card.expiry }}
</div>
</div>
</VCardText>
</VCard>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<!-- 👉 Billing Address -->
<VCard title="Billing Address">
<template #append>
<VBtn
size="small"
prepend-icon="tabler-plus"
@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">
{{ currentAddress.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">
{{ currentAddress.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">
{{ currentAddress.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">
{{ currentAddress.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-2">
{{ currentAddress.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">
{{ currentAddress.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">
{{ currentAddress.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">
{{ currentAddress.state }}
</p>
</td>
</tr>
<tr>
<td>
<h6 class="text-h6 text-no-wrap mb-2">
Zip Code:
</h6>
</td>
<td>
<p class="text-body-1 mb-2">
{{ currentAddress.zipCode }}
</p>
</td>
</tr>
</VTable>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- 👉 Edit Card Dialog -->
<CardAddEditDialog
v-model:is-dialog-visible="isCardEditDialogVisible"
:card-details="currentCardDetails"
/>
<!-- 👉 Add Card Dialog -->
<CardAddEditDialog v-model:is-dialog-visible="isCardAddDialogVisible" />
<!-- 👉 Edit Address dialog -->
<AddEditAddressDialog
v-model:is-dialog-visible="isEditAddressDialogVisible"
:billing-address="currentBillingAddress"
/>
<!-- 👉 Upgrade plan dialog -->
<UserUpgradePlanDialog v-model:is-dialog-visible="isUpgradePlanDialogVisible" />
</template>
<style lang="scss">
.billing-address-table {
tr {
td:first-child {
inline-size: 148px;
}
}
}
</style>

View File

@@ -0,0 +1,181 @@
<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/pixinvents',
username: '@Pixinvent',
connected: true,
},
{
img: linkedin,
title: 'LinkedIn',
link: 'https://www.linkedin.com/company/pixinvent',
username: '@Pixinvent',
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"
:subtitle="account.text"
>
<template #title>
<h6 class="text-h6">
{{ account.title }}
</h6>
</template>
<template #prepend>
<VAvatar
start
:size="36"
:image="account.img"
class="me-1"
/>
</template>
<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"
>
<h6 class="text-h6">
{{ account.title }}
</h6>
<template #prepend>
<VAvatar
start
size="36"
rounded="0"
class="me-1"
:image="account.img"
/>
</template>
<VListItemSubtitle v-if="account.connected">
<a
:href="account.link"
target="_blank"
rel="noopener noreferrer"
>
{{ account.username }}
</a>
</VListItemSubtitle>
<VListItemSubtitle v-else>
Not connected
</VListItemSubtitle>
<template #append>
<IconBtn
variant="tonal"
:color="account.connected ? 'error' : 'secondary'"
class="rounded"
>
<VIcon :icon="account.connected ? 'tabler-trash' : 'tabler-link'" />
</IconBtn>
</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,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
class="user-tab-notification"
title="Notifications"
subtitle="You will receive notification for the below selected items."
>
<VCardText class="px-0">
<VDivider />
<VTable class="text-no-wrap">
<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>
<VCardText class="d-flex flex-wrap gap-4">
<VBtn>Save changes</VBtn>
<VBtn
color="secondary"
variant="tonal"
>
Discard
</VBtn>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,188 @@
<script setup>
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',
icon: 'tabler-brand-windows',
color: 'info',
device: 'HP Spectre 360',
location: 'Switzerland',
activity: '10, July 2021 20:07',
},
{
browser: 'Chrome on Android',
icon: 'tabler-brand-android',
color: 'success',
device: 'Oneplus 9 Pro',
location: 'Dubai',
activity: '14, July 2021 15:15',
},
{
browser: 'Chrome on macOS',
icon: 'tabler-brand-apple',
color: 'secondary',
device: 'Apple iMac',
location: 'India',
activity: '16, July 2021 16:17',
},
{
browser: 'Chrome on iPhone',
icon: 'tabler-device-mobile',
color: 'error',
device: 'iPhone 12x',
location: 'Australia',
activity: '13, July 2021 10:10',
},
]
</script>
<template>
<VRow>
<VCol cols="12">
<!-- 👉 Change password -->
<VCard title="Change Password">
<VCardText>
<VAlert
closable
variant="tonal"
color="warning"
class="mb-4"
title="Ensure that these requirements are met"
text="Minimum 8 characters long, uppercase & symbol"
/>
<VForm @submit.prevent="() => { }">
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
label="New Password"
placeholder="············"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
label="Confirm Password"
autocomplete="confirm-password"
placeholder="············"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@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-steps verification"
subtitle="Keep your account secure with authentication step."
>
<VCardText>
<div class="text-h6 mb-1">
SMS
</div>
<AppTextField placeholder="+1(968) 819-2547">
<template #append>
<IconBtn color="secondary">
<VIcon
icon="tabler-edit"
size="22"
/>
</IconBtn>
<IconBtn color="secondary">
<VIcon
icon="tabler-user-plus"
size="22"
/>
</IconBtn>
</template>
</AppTextField>
<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">
<VDivider />
<VDataTable
:items="recentDevices"
:headers="recentDeviceHeader"
hide-default-footer
class="text-no-wrap"
>
<template #item.browser="{ item }">
<div class="d-flex align-center gap-x-4">
<VIcon
:icon="item.icon"
:color="item.color"
:size="22"
/>
<div class="text-body-1 text-high-emphasis">
{{ item.browser }}
</div>
</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:is-dialog-visible="isTwoFactorDialogOpen"
:sms-code="smsVerificationNumber"
/>
</template>

View File

@@ -0,0 +1,73 @@
<script setup>
import { useTheme } from 'vuetify'
import { getAreaChartSplineConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getAreaChartSplineConfig(vuetifyTheme.current.value))
const series = [
{
name: 'Visits',
data: [
100,
120,
90,
170,
130,
160,
140,
240,
220,
180,
270,
280,
375,
],
},
{
name: 'Clicks',
data: [
60,
80,
70,
110,
80,
100,
90,
180,
160,
140,
200,
220,
275,
],
},
{
name: 'Sales',
data: [
20,
40,
30,
70,
40,
60,
50,
140,
120,
100,
140,
180,
220,
],
},
]
</script>
<template>
<VueApexCharts
type="area"
height="400"
:options="chartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { useTheme } from 'vuetify'
import { getLineChartSimpleConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const balanceChartConfig = computed(() => getLineChartSimpleConfig(vuetifyTheme.current.value))
const series = [{
data: [
280,
200,
220,
180,
270,
250,
70,
90,
200,
150,
160,
100,
150,
100,
50,
],
}]
</script>
<template>
<VueApexCharts
type="line"
height="400"
:options="balanceChartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,85 @@
<script setup>
import { useTheme } from 'vuetify'
import { getHeatMapChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getHeatMapChartConfig(vuetifyTheme.current.value))
const generateDataHeat = (count, yrange) => {
let i = 0
const series = []
while (i < count) {
const x = `w${ (i + 1).toString() }`
const y = Math.floor(Math.random() * (yrange.max - yrange.min + 1)) + yrange.min
series.push({
x,
y,
})
i += 1
}
return series
}
const series = [
{
name: 'SUN',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'MON',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'TUE',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'WED',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'THU',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'FRI',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
{
name: 'SAT',
data: generateDataHeat(24, {
min: 0,
max: 60,
}),
},
]
</script>
<template>
<VueApexCharts
type="heatmap"
height="350"
:options="chartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,47 @@
<script setup>
import { useTheme } from 'vuetify'
import { getColumnChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getColumnChartConfig(vuetifyTheme.current.value))
const series = [
{
name: 'Apple',
data: [
90,
120,
55,
100,
80,
125,
175,
70,
88,
],
},
{
name: 'Samsung',
data: [
85,
100,
30,
40,
95,
90,
30,
110,
62,
],
},
]
</script>
<template>
<VueApexCharts
type="bar"
height="400"
:options="chartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { useTheme } from 'vuetify'
import { getDonutChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const expenseRationChartConfig = computed(() => getDonutChartConfig(vuetifyTheme.current.value))
const series = [
85,
16,
50,
50,
]
</script>
<template>
<VueApexCharts
type="donut"
height="410"
:options="expenseRationChartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
import { useTheme } from 'vuetify'
import { getBarChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const horizontalBarChartConfig = computed(() => getBarChartConfig(vuetifyTheme.current.value))
const series = [{
data: [
700,
350,
480,
600,
210,
550,
150,
],
}]
</script>
<template>
<VueApexCharts
type="bar"
height="400"
:options="horizontalBarChartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,46 @@
<script setup>
import { useTheme } from 'vuetify'
import { getRadarChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const series = [
{
name: 'iPhone 12',
data: [
41,
64,
81,
60,
42,
42,
33,
23,
],
},
{
name: 'Samsung s20',
data: [
65,
46,
42,
25,
58,
63,
76,
43,
],
},
]
const chartConfig = computed(() => getRadarChartConfig(vuetifyTheme.current.value))
</script>
<template>
<VueApexCharts
type="radar"
height="400"
:options="chartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,178 @@
<script setup>
import { useTheme } from 'vuetify'
import { getScatterChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const scatterChartConfig = computed(() => getScatterChartConfig(vuetifyTheme.current.value))
const series = [
{
name: 'Angular',
data: [
{
x: 5.4,
y: 170,
},
{
x: 5.4,
y: 100,
},
{
x: 6.3,
y: 170,
},
{
x: 5.7,
y: 140,
},
{
x: 5.9,
y: 130,
},
{
x: 7,
y: 150,
},
{
x: 8,
y: 120,
},
{
x: 9,
y: 170,
},
{
x: 10,
y: 190,
},
{
x: 11,
y: 220,
},
{
x: 12,
y: 170,
},
{
x: 13,
y: 230,
},
],
},
{
name: 'Vue',
data: [
{
x: 14,
y: 220,
},
{
x: 15,
y: 280,
},
{
x: 16,
y: 230,
},
{
x: 18,
y: 320,
},
{
x: 17.5,
y: 280,
},
{
x: 19,
y: 250,
},
{
x: 20,
y: 350,
},
{
x: 20.5,
y: 320,
},
{
x: 20,
y: 320,
},
{
x: 19,
y: 280,
},
{
x: 17,
y: 280,
},
{
x: 22,
y: 300,
},
{
x: 18,
y: 120,
},
],
},
{
name: 'React',
data: [
{
x: 14,
y: 290,
},
{
x: 13,
y: 190,
},
{
x: 20,
y: 220,
},
{
x: 21,
y: 350,
},
{
x: 21.5,
y: 290,
},
{
x: 22,
y: 220,
},
{
x: 23,
y: 140,
},
{
x: 19,
y: 400,
},
{
x: 20,
y: 200,
},
{
x: 22,
y: 90,
},
{
x: 20,
y: 120,
},
],
},
]
</script>
<template>
<VueApexCharts
type="scatter"
height="400"
:options="scatterChartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,22 @@
<script setup>
import { useTheme } from 'vuetify'
import { getRadialBarChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const statisticsChartConfig = computed(() => getRadialBarChartConfig(vuetifyTheme.current.value))
const series = [
80,
50,
35,
]
</script>
<template>
<VueApexCharts
type="radialBar"
height="400"
:options="statisticsChartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,147 @@
<script setup>
import { useTheme } from 'vuetify'
import { getCandlestickChartConfig } from '@core/libs/apex-chart/apexCharConfig'
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getCandlestickChartConfig(vuetifyTheme.current.value))
const series = [{
data: [
{
x: `7/12/${ new Date().getFullYear() }`,
y: [
150,
170,
50,
100,
],
},
{
x: `8/12/${ new Date().getFullYear() }`,
y: [
200,
400,
170,
330,
],
},
{
x: `9/12/${ new Date().getFullYear() }`,
y: [
330,
340,
250,
280,
],
},
{
x: `10/12/${ new Date().getFullYear() }`,
y: [
300,
330,
200,
320,
],
},
{
x: `11/12/${ new Date().getFullYear() }`,
y: [
320,
450,
280,
350,
],
},
{
x: `12/12/${ new Date().getFullYear() }`,
y: [
300,
350,
80,
250,
],
},
{
x: `13/12/${ new Date().getFullYear() }`,
y: [
200,
330,
170,
300,
],
},
{
x: `14/12/${ new Date().getFullYear() }`,
y: [
200,
220,
70,
130,
],
},
{
x: `15/12/${ new Date().getFullYear() }`,
y: [
220,
270,
180,
250,
],
},
{
x: `16/12/${ new Date().getFullYear() }`,
y: [
200,
250,
80,
100,
],
},
{
x: `17/12/${ new Date().getFullYear() }`,
y: [
150,
170,
50,
120,
],
},
{
x: `18/12/${ new Date().getFullYear() }`,
y: [
110,
450,
10,
420,
],
},
{
x: `19/12/${ new Date().getFullYear() }`,
y: [
400,
480,
300,
320,
],
},
{
x: `20/12/${ new Date().getFullYear() }`,
y: [
380,
480,
350,
450,
],
},
],
}]
</script>
<template>
<VueApexCharts
type="candlestick"
height="385"
:options="chartConfig"
:series="series"
/>
</template>

View File

@@ -0,0 +1,65 @@
<script setup>
import { useTheme } from 'vuetify'
import { getLatestBarChartConfig } from '@core/libs/chartjs/chartjsConfig'
import BarChart from '@core/libs/chartjs/components/BarChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const chartOptions = computed(() => getLatestBarChartConfig(vuetifyTheme.current.value))
const data = {
labels: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
],
datasets: [{
maxBarThickness: 15,
backgroundColor: props.colors.barChartYellow,
borderColor: 'transparent',
borderRadius: {
topRight: 15,
topLeft: 15,
},
data: [
275,
90,
190,
205,
125,
85,
55,
87,
127,
150,
230,
280,
190,
],
}],
}
</script>
<template>
<BarChart
:height="400"
:chart-data="data"
:chart-options="chartOptions"
/>
</template>

View File

@@ -0,0 +1,173 @@
<script setup>
import { useTheme } from 'vuetify'
import { getBubbleChartConfig } from '@core/libs/chartjs/chartjsConfig'
import BubbleChart from '@core/libs/chartjs/components/BubbleChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getBubbleChartConfig(vuetifyTheme.current.value))
const data = {
animation: { duration: 10000 },
datasets: [
{
label: 'Dataset 1',
borderColor: props.colors.primary,
backgroundColor: props.colors.primary,
data: [
{
x: 20,
y: 74,
r: 10,
},
{
x: 10,
y: 110,
r: 5,
},
{
x: 30,
y: 165,
r: 7,
},
{
x: 40,
y: 200,
r: 20,
},
{
x: 90,
y: 185,
r: 7,
},
{
x: 50,
y: 240,
r: 7,
},
{
x: 60,
y: 275,
r: 10,
},
{
x: 70,
y: 305,
r: 5,
},
{
x: 80,
y: 325,
r: 4,
},
{
x: 100,
y: 310,
r: 5,
},
{
x: 110,
y: 240,
r: 5,
},
{
x: 120,
y: 270,
r: 7,
},
{
x: 130,
y: 300,
r: 6,
},
],
},
{
label: 'Dataset 2',
borderColor: props.colors.yellow,
backgroundColor: props.colors.yellow,
data: [
{
x: 30,
y: 72,
r: 5,
},
{
x: 40,
y: 110,
r: 7,
},
{
x: 20,
y: 135,
r: 6,
},
{
x: 10,
y: 160,
r: 12,
},
{
x: 50,
y: 285,
r: 5,
},
{
x: 60,
y: 235,
r: 5,
},
{
x: 70,
y: 275,
r: 7,
},
{
x: 80,
y: 290,
r: 4,
},
{
x: 90,
y: 250,
r: 10,
},
{
x: 100,
y: 220,
r: 7,
},
{
x: 120,
y: 230,
r: 4,
},
{
x: 110,
y: 320,
r: 15,
},
{
x: 130,
y: 330,
r: 7,
},
],
},
],
}
</script>
<template>
<BubbleChart
:height="400"
:chart-data="data"
:chart-options="chartConfig"
/>
</template>

View File

@@ -0,0 +1,61 @@
<script setup>
import { useTheme } from 'vuetify'
import { getHorizontalBarChartConfig } from '@core/libs/chartjs/chartjsConfig'
import BarChart from '@core/libs/chartjs/components/BarChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const chartOptions = computed(() => getHorizontalBarChartConfig(vuetifyTheme.current.value))
const data = {
labels: [
'MON',
'TUE',
'WED ',
'THU',
'FRI',
],
datasets: [
{
maxBarThickness: 15,
label: 'Market Data',
backgroundColor: props.colors.warningShade,
borderColor: 'transparent',
data: [
710,
350,
580,
460,
120,
],
},
{
maxBarThickness: 15,
backgroundColor: props.colors.horizontalBarInfo,
label: 'Personal Data',
borderColor: 'transparent',
data: [
430,
590,
510,
240,
360,
],
},
],
}
</script>
<template>
<BarChart
:height="375"
:chart-data="data"
:chart-options="chartOptions"
/>
</template>

View File

@@ -0,0 +1,139 @@
<script setup>
import { useTheme } from 'vuetify'
import { getLineAreaChartConfig } from '@core/libs/chartjs/chartjsConfig'
import LineChart from '@core/libs/chartjs/components/LineChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const data = {
labels: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
'20/12',
'',
],
datasets: [
{
fill: true,
tension: 0,
label: 'Africa',
pointRadius: 0.5,
pointHoverRadius: 5,
pointStyle: 'circle',
backgroundColor: props.colors.areaChartBlue,
pointHoverBorderWidth: 5,
borderColor: 'transparent',
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.areaChartBlue,
data: [
40,
55,
45,
75,
65,
55,
70,
60,
100,
98,
90,
120,
125,
140,
155,
],
},
{
fill: true,
tension: 0,
label: 'Asia',
pointRadius: 0.5,
pointHoverRadius: 5,
pointStyle: 'circle',
pointHoverBorderWidth: 5,
borderColor: 'transparent',
backgroundColor: props.colors.areaChartBlueLight,
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.areaChartBlueLight,
data: [
70,
85,
75,
150,
100,
140,
110,
105,
160,
150,
125,
190,
200,
240,
275,
],
},
{
fill: true,
tension: 0,
label: 'Europe',
pointRadius: 0.5,
pointHoverRadius: 5,
pointStyle: 'circle',
pointHoverBorderWidth: 5,
borderColor: 'transparent',
backgroundColor: props.colors.areaChartGreyLight,
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.areaChartGreyLight,
data: [
240,
195,
160,
215,
185,
215,
185,
200,
250,
210,
195,
250,
235,
300,
315,
],
},
],
}
const chartConfig = computed(() => getLineAreaChartConfig(vuetifyTheme.current.value))
</script>
<template>
<LineChart
:chart-options="chartConfig"
:height="400"
:chart-data="data"
/>
</template>

View File

@@ -0,0 +1,139 @@
<script setup>
import { useTheme } from 'vuetify'
import { getLineChartConfig } from '@core/libs/chartjs/chartjsConfig'
import LineChart from '@core/libs/chartjs/components/LineChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const data = {
labels: [
0,
10,
20,
30,
40,
50,
60,
70,
80,
90,
100,
110,
120,
130,
140,
],
datasets: [
{
fill: false,
tension: 0.5,
pointRadius: 1,
label: 'Europe',
pointHoverRadius: 5,
pointStyle: 'circle',
borderColor: props.colors.primary,
backgroundColor: props.colors.primary,
pointHoverBorderWidth: 5,
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.primary,
data: [
80,
150,
180,
270,
210,
160,
160,
202,
265,
210,
270,
255,
290,
360,
375,
],
},
{
fill: false,
tension: 0.5,
label: 'Asia',
pointRadius: 1,
pointHoverRadius: 5,
pointStyle: 'circle',
borderColor: props.colors.warningShade,
backgroundColor: props.colors.warningShade,
pointHoverBorderWidth: 5,
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.warningShade,
data: [
80,
125,
105,
130,
215,
195,
140,
160,
230,
300,
220,
170,
210,
200,
280,
],
},
{
fill: false,
tension: 0.5,
pointRadius: 1,
label: 'Africa',
pointHoverRadius: 5,
pointStyle: 'circle',
borderColor: props.colors.yellow,
backgroundColor: props.colors.yellow,
pointHoverBorderWidth: 5,
pointHoverBorderColor: props.colors.white,
pointBorderColor: 'transparent',
pointHoverBackgroundColor: props.colors.yellow,
data: [
80,
99,
82,
90,
115,
115,
74,
75,
130,
155,
125,
90,
140,
130,
180,
],
},
],
}
const chartConfig = computed(() => getLineChartConfig(vuetifyTheme.current.value))
</script>
<template>
<LineChart
:chart-options="chartConfig"
:height="400"
:chart-data="data"
/>
</template>

View File

@@ -0,0 +1,54 @@
<script setup>
import { useTheme } from 'vuetify'
import { getPolarChartConfig } from '@core/libs/chartjs/chartjsConfig'
import PolarAreaChart from '@core/libs/chartjs/components/PolarAreaChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getPolarChartConfig(vuetifyTheme.current.value))
const data = {
labels: [
'Africa',
'Asia',
'Europe',
'America',
'Antarctica',
'Australia',
],
datasets: [{
borderWidth: 0,
label: 'Population (millions)',
data: [
19,
17.5,
15,
13.5,
11,
9,
],
backgroundColor: [
props.colors.primary,
props.colors.yellow,
props.colors.polarChartWarning,
props.colors.polarChartInfo,
props.colors.polarChartGrey,
props.colors.polarChartGreen,
],
}],
}
</script>
<template>
<PolarAreaChart
:height="400"
:chart-data="data"
:chart-options="chartConfig"
/>
</template>

View File

@@ -0,0 +1,61 @@
<script setup>
import { useTheme } from 'vuetify'
import { getRadarChartConfig } from '@core/libs/chartjs/chartjsConfig'
import RadarChart from '@core/libs/chartjs/components/RadarChart'
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getRadarChartConfig(vuetifyTheme.current.value))
const chartData = {
labels: [
'STA',
'STR',
'AGI',
'VIT',
'CHA',
'INT',
],
datasets: [
{
fill: true,
label: 'Donté Panlin',
borderColor: 'transparent',
backgroundColor: 'rgba(255,161,161, 0.9)',
data: [
25,
59,
90,
81,
60,
82,
],
pointBorderColor: 'transparent',
pointBackgroundColor: 'transparent',
},
{
fill: true,
label: 'Mireska Sunbreeze',
borderColor: 'transparent',
backgroundColor: 'rgba(155,136,250, 0.9)',
data: [
40,
100,
40,
90,
40,
90,
],
pointBorderColor: 'transparent',
pointBackgroundColor: 'transparent',
},
],
}
</script>
<template>
<RadarChart
:height="400"
:chart-data="chartData"
:chart-options="chartConfig"
/>
</template>

View File

@@ -0,0 +1,238 @@
<script setup>
import { useTheme } from 'vuetify'
import { getScatterChartConfig } from '@core/libs/chartjs/chartjsConfig'
import ScatterChart from '@core/libs/chartjs/components/ScatterChart'
const props = defineProps({
colors: {
type: null,
required: true,
},
})
const vuetifyTheme = useTheme()
const chartConfig = computed(() => getScatterChartConfig(vuetifyTheme.current.value))
const data = {
datasets: [
{
pointRadius: 5,
label: 'iPhone',
pointBorderWidth: 2,
backgroundColor: props.colors.primary,
pointHoverBorderWidth: 2,
borderColor: 'transparent',
data: [
{
x: 72,
y: 225,
},
{
x: 81,
y: 270,
},
{
x: 90,
y: 230,
},
{
x: 103,
y: 305,
},
{
x: 103,
y: 245,
},
{
x: 108,
y: 275,
},
{
x: 110,
y: 290,
},
{
x: 111,
y: 315,
},
{
x: 109,
y: 350,
},
{
x: 116,
y: 340,
},
{
x: 113,
y: 260,
},
{
x: 117,
y: 275,
},
{
x: 117,
y: 295,
},
{
x: 126,
y: 280,
},
{
x: 127,
y: 340,
},
{
x: 133,
y: 330,
},
],
},
{
pointRadius: 5,
pointBorderWidth: 2,
label: 'Samsung Note',
backgroundColor: props.colors.scatterChartWarning,
pointHoverBorderWidth: 2,
borderColor: 'transparent',
data: [
{
x: 13,
y: 95,
},
{
x: 22,
y: 105,
},
{
x: 17,
y: 115,
},
{
x: 19,
y: 130,
},
{
x: 21,
y: 125,
},
{
x: 35,
y: 125,
},
{
x: 13,
y: 155,
},
{
x: 21,
y: 165,
},
{
x: 25,
y: 155,
},
{
x: 18,
y: 190,
},
{
x: 26,
y: 180,
},
{
x: 43,
y: 180,
},
{
x: 53,
y: 202,
},
{
x: 61,
y: 165,
},
{
x: 67,
y: 225,
},
],
},
{
pointRadius: 5,
label: 'OnePlus',
pointBorderWidth: 2,
backgroundColor: props.colors.scatterChartGreen,
pointHoverBorderWidth: 2,
borderColor: 'transparent',
data: [
{
x: 70,
y: 195,
},
{
x: 72,
y: 270,
},
{
x: 98,
y: 255,
},
{
x: 100,
y: 215,
},
{
x: 87,
y: 240,
},
{
x: 94,
y: 280,
},
{
x: 99,
y: 300,
},
{
x: 102,
y: 290,
},
{
x: 110,
y: 275,
},
{
x: 111,
y: 250,
},
{
x: 94,
y: 280,
},
{
x: 92,
y: 340,
},
{
x: 100,
y: 335,
},
{
x: 108,
y: 330,
},
],
},
],
}
</script>
<template>
<ScatterChart
:height="380"
:chart-data="data"
:chart-options="chartConfig"
/>
</template>

View File

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

View File

@@ -0,0 +1,85 @@
<script setup>
import { useTheme } from 'vuetify'
const vuetifyTheme = useTheme()
const currentTheme = vuetifyTheme.current.value.colors
const series = [{
data: [
400,
200,
650,
500,
],
}]
const chartOptions = {
chart: {
type: 'area',
toolbar: { show: false },
sparkline: { enabled: true },
},
markers: {
colors: 'transparent',
strokeColors: 'transparent',
},
grid: { show: false },
colors: [currentTheme.success],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 0.8,
opacityFrom: 0.6,
opacityTo: 0.1,
},
},
dataLabels: { enabled: false },
stroke: {
width: 2,
curve: 'smooth',
},
xaxis: {
show: true,
lines: { show: false },
labels: { show: false },
stroke: { width: 0 },
axisBorder: { show: false },
},
yaxis: {
stroke: { width: 0 },
show: false,
},
responsive: [
{
breakpoint: 1387,
options: { chart: { height: 80 } },
},
{
breakpoint: 1200,
options: { chart: { height: 120 } },
},
],
}
</script>
<template>
<VCard>
<VCardText>
<h5 class="text-h5 mb-3">
Average Daily Sales
</h5>
<p class="mb-0">
Total Sales This Month
</p>
<h4 class="text-h4">
$28,450
</h4>
</VCardText>
<VueApexCharts
:options="chartOptions"
:series="series"
:height="80"
/>
</VCard>
</template>

View File

@@ -0,0 +1,220 @@
<script setup>
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
const vuetifyTheme = useTheme()
const series = [{
data: [
40,
65,
50,
45,
90,
55,
70,
],
}]
const chartOptions = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
const variableTheme = vuetifyTheme.current.value.variables
return {
chart: {
parentHeightOffset: 0,
type: 'bar',
toolbar: { show: false },
},
plotOptions: {
bar: {
barHeight: '60%',
columnWidth: '38%',
startingShape: 'rounded',
endingShape: 'rounded',
borderRadius: 4,
distributed: true,
},
},
grid: {
show: false,
padding: {
top: -30,
bottom: 0,
left: -10,
right: -10,
},
},
colors: [
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
`rgba(${ hexToRgb(currentTheme.primary) }, 1)`,
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
`rgba(${ hexToRgb(currentTheme.primary) },${ variableTheme['dragged-opacity'] })`,
],
dataLabels: { enabled: false },
legend: { show: false },
xaxis: {
categories: [
'Mo',
'Tu',
'We',
'Th',
'Fr',
'Sa',
'Su',
],
axisBorder: { show: false },
axisTicks: { show: false },
labels: {
style: {
colors: `rgba(${ hexToRgb(currentTheme['on-surface']) },${ variableTheme['disabled-opacity'] })`,
fontSize: '13px',
fontFamily: 'Public Sans',
},
},
},
yaxis: { labels: { show: false } },
tooltip: { enabled: false },
responsive: [{
breakpoint: 1025,
options: { chart: { height: 199 } },
}],
}
})
const earningsReports = [
{
color: 'primary',
icon: 'tabler-currency-dollar',
title: 'Earnings',
amount: '$545.69',
progress: '55',
},
{
color: 'info',
icon: 'tabler-chart-pie-2',
title: 'Profit',
amount: '$256.34',
progress: '25',
},
{
color: 'error',
icon: 'tabler-brand-paypal',
title: 'Expense',
amount: '$74.19',
progress: '65',
},
]
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard>
<VCardItem class="pb-sm-0">
<VCardTitle>Earning Reports</VCardTitle>
<VCardSubtitle>Weekly Earnings Overview</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VRow>
<VCol
cols="12"
sm="5"
lg="6"
class="d-flex flex-column align-self-center"
>
<div class="d-flex align-center gap-2 mb-3 flex-wrap">
<h2 class="text-h2">
$468
</h2>
<VChip
label
size="small"
color="success"
>
+4.2%
</VChip>
</div>
<span class="text-sm text-medium-emphasis">
You informed of this week compared to last week
</span>
</VCol>
<VCol
cols="12"
sm="7"
lg="6"
>
<VueApexCharts
:options="chartOptions"
:series="series"
height="161"
/>
</VCol>
</VRow>
<div class="border rounded mt-5 pa-5">
<VRow>
<VCol
v-for="report in earningsReports"
:key="report.title"
cols="12"
sm="4"
>
<div class="d-flex align-center">
<VAvatar
rounded
size="26"
:color="report.color"
variant="tonal"
class="me-2"
>
<VIcon
size="18"
:icon="report.icon"
/>
</VAvatar>
<h6 class="text-base font-weight-regular">
{{ report.title }}
</h6>
</div>
<h6 class="text-h4 my-2">
{{ report.amount }}
</h6>
<VProgressLinear
:model-value="report.progress"
:color="report.color"
height="4"
rounded
rounded-bar
/>
</VCol>
</VRow>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,131 @@
<script setup>
const monthlyCampaignState = [
{
avatarColor: 'success',
avatarIcon: 'tabler-mail',
title: 'Emails',
count: '12,346',
stats: '0.3%',
statsColor: 'success',
},
{
avatarColor: 'info',
avatarIcon: 'tabler-link',
title: 'Opened',
count: '8,734',
stats: '2.1%',
statsColor: 'success',
},
{
avatarColor: 'warning',
avatarIcon: 'tabler-mouse',
title: 'Clicked',
count: '967',
stats: '1.4%',
statsColor: 'error',
},
{
avatarColor: 'primary',
avatarIcon: 'tabler-users',
title: 'Subscribe',
count: '345',
stats: '8.5%',
statsColor: 'success',
},
{
avatarColor: 'secondary',
avatarIcon: 'tabler-alert-triangle',
title: 'Complaints',
count: '10',
stats: '1.5%',
statsColor: 'error',
},
{
avatarColor: 'error',
avatarIcon: 'tabler-ban',
title: 'Unsubscribe',
count: '86',
stats: '0.8%',
statsColor: 'success',
},
]
const moreList = [
{
title: 'Refresh',
value: 'refresh',
},
{
title: 'Download',
value: 'Download',
},
{
title: 'View All',
value: 'View All',
},
]
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>Monthly Campaign State</VCardTitle>
<VCardSubtitle>
8.52k Social Visitors
</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="state in monthlyCampaignState"
:key="state.title"
>
<template #prepend>
<VAvatar
:color="state.avatarColor"
variant="tonal"
size="34"
rounded
class="me-1"
>
<VIcon
:icon="state.avatarIcon"
size="22"
/>
</VAvatar>
</template>
<VListItemTitle class="font-weight-medium me-4">
{{ state.title }}
</VListItemTitle>
<template #append>
<div class="d-flex gap-x-4">
<div class="text-body-1">
{{ state.count }}
</div>
<div :class="`text-${state.statsColor}`">
{{ state.stats }}
</div>
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,186 @@
<script setup>
const projectTableHeaders = [
{
title: 'PROJECT',
key: 'project',
},
{
title: 'LEADER',
key: 'leader',
},
{
title: 'Team',
key: 'team',
sortable: false,
},
{
title: 'PROGRESS',
key: 'progress',
},
{
title: 'Action',
key: 'Action',
sortable: false,
},
]
const search = ref('')
const itemsPerPage = ref(5)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const { data: projectsData } = await useApi(createUrl('/dashboard/analytics/projects', {
query: {
q: search,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const updateOptions = options => {
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const projects = computed(() => projectsData.value?.projects)
const totalProjects = computed(() => projectsData.value?.totalProjects)
const moreList = [
{
title: 'Download',
value: 'Download',
},
{
title: 'Delete',
value: 'Delete',
},
{
title: 'View',
value: 'View',
},
]
</script>
<template>
<VCard v-if="projects">
<VCardItem class="project-header d-flex flex-wrap justify-space-between gap-4">
<VCardTitle>Project List</VCardTitle>
<template #append>
<div style="inline-size: 250px;">
<AppTextField
v-model="search"
placeholder="Search Project"
/>
</div>
</template>
</VCardItem>
<VDivider />
<!-- SECTION Table -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:items="projects"
:items-length="totalProjects"
item-value="name"
:headers="projectTableHeaders"
class="text-no-wrap"
show-select
@update:options="updateOptions"
>
<!-- projects -->
<template #item.project="{ item }">
<div
class="d-flex align-center gap-x-3"
style="padding-block: 7px;"
>
<VAvatar
:size="34"
:image="item.logo"
/>
<div>
<h6 class="text-h6 text-no-wrap">
{{ item.name }}
</h6>
<div class="text-body-2">
{{ item.project }}
</div>
</div>
</div>
</template>
<template #item.leader="{ item }">
<div class="text-base text-high-emphasis">
{{ item.leader }}
</div>
</template>
<!-- Team -->
<template #item.team="{ item }">
<div class="d-flex">
<div class="v-avatar-group">
<VAvatar
v-for="(data, index) in item.team"
:key="index"
size="26"
>
<VImg :src="data" />
</VAvatar>
<VAvatar
v-if="item.extraMembers"
:color="$vuetify.theme.current.dark ? '#373b50' : '#eeedf0'"
:size="26"
>
<div class="text-caption text-high-emphasis">
+{{ item.extraMembers }}
</div>
</VAvatar>
</div>
</div>
</template>
<!-- Progress -->
<template #item.progress="{ item }">
<div class="d-flex align-center gap-3">
<div class="flex-grow-1">
<VProgressLinear
:height="6"
:model-value="item.progress"
color="primary"
rounded
/>
</div>
<div class="text-body-1 text-high-emphasis">
{{ item.progress }}%
</div>
</div>
</template>
<!-- Action -->
<template #item.Action>
<MoreBtn :menu-list="moreList" />
</template>
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
<template #bottom>
<TablePagination
v-model:page="page"
:items-per-page="itemsPerPage"
:total-items="totalProjects"
/>
</template>
</VDataTableServer>
<!-- !SECTION -->
</VCard>
</template>
<style lang="scss">
.project-header .v-card-item__append {
padding-inline-start: 0;
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup>
import auFlag from '@images/icons/countries/au.png'
import brFlag from '@images/icons/countries/br.png'
import cnFlag from '@images/icons/countries/cn.png'
import frFlag from '@images/icons/countries/fr.png'
import inFlag from '@images/icons/countries/in.png'
import usFlag from '@images/icons/countries/us.png'
const salesByCountries = [
{
avatarImg: usFlag,
stats: '$8,567k',
subtitle: 'United states',
profitLoss: 25.8,
},
{
avatarImg: brFlag,
stats: '$2,415k',
subtitle: 'Brazil',
profitLoss: -6.2,
},
{
avatarImg: inFlag,
stats: '$865k',
subtitle: 'India',
profitLoss: 12.4,
},
{
avatarImg: auFlag,
stats: '$745k',
subtitle: 'Australia',
profitLoss: -11.9,
},
{
avatarImg: frFlag,
stats: '$45',
subtitle: 'France',
profitLoss: 16.2,
},
{
avatarImg: cnFlag,
stats: '$12k',
subtitle: 'China',
profitLoss: 14.8,
},
]
const moreList = [
{
title: 'Refresh',
value: 'refresh',
},
{
title: 'Download',
value: 'Download',
},
{
title: 'View All',
value: 'View All',
},
]
</script>
<template>
<VCard
title="Sales by Countries"
subtitle="Monthly Sales Overview"
>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
<VCardText>
<VList class="card-list">
<VListItem
v-for="country in salesByCountries"
:key="country.stats"
>
<template #prepend>
<VAvatar
size="34"
color="secondary"
variant="tonal"
class="me-1"
:image="country.avatarImg"
/>
</template>
<VListItemTitle class="font-weight-medium">
{{ country.stats }}
</VListItemTitle>
<VListItemSubtitle>
{{ country.subtitle }}
</VListItemSubtitle>
<template #append>
<div :class="`d-flex align-center ${country.profitLoss > 0 ? 'text-success' : 'text-error'}`">
<VIcon
:icon="country.profitLoss > 0 ? 'tabler-chevron-up' : 'tabler-chevron-down'"
size="20"
class="me-1"
/>
<div class="font-weight-medium">
{{ Math.abs(country.profitLoss) }}%
</div>
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1rem;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<VCard>
<VCardText>
<div class="d-flex align-center justify-space-between">
<div class="text-body-1">
Sales Overview
</div>
<div class="text-success font-weight-medium">
+18.2%
</div>
</div>
<h4 class="text-h4">
$42.5k
</h4>
</VCardText>
<VCardText>
<VRow no-gutters>
<VCol cols="5">
<div class="py-2">
<div class="d-flex align-center mb-3">
<VAvatar
color="info"
variant="tonal"
:size="24"
rounded
class="me-2"
>
<VIcon
size="18"
icon="tabler-shopping-cart"
/>
</VAvatar>
<span>Order</span>
</div>
<h5 class="text-h5">
62.2%
</h5>
<div class="text-body-2 text-disabled">
6,440
</div>
</div>
</VCol>
<VCol cols="2">
<div class="d-flex flex-column align-center justify-center h-100">
<VDivider
vertical
class="mx-auto"
/>
<VAvatar
size="24"
color="rgba(var(--v-theme-on-surface), var(--v-hover-opacity))"
class="my-2"
>
<div class="text-overline text-disabled">
VS
</div>
</VAvatar>
<VDivider
vertical
class="mx-auto"
/>
</div>
</VCol>
<VCol
cols="5"
class="text-end"
>
<div class="py-2">
<div class="d-flex align-center justify-end mb-3">
<span class="me-2">Visits</span>
<VAvatar
color="primary"
variant="tonal"
:size="24"
rounded
>
<VIcon
size="18"
icon="tabler-link"
/>
</VAvatar>
</div>
<h5 class="text-h5">
25.5%
</h5>
<div class="text-body-2 text-disabled">
12,749
</div>
</div>
</VCol>
</VRow>
<div class="mt-6">
<VProgressLinear
model-value="72"
color="#00CFE8"
height="10"
bg-color="primary"
:rounded-bar="false"
rounded
/>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,133 @@
<script setup>
const sourceVisits = [
{
avatarIcon: 'tabler-shadow',
title: 'Direct Source',
subtitle: 'Direct link click',
stats: '1.2k',
profitLoss: 4.2,
},
{
avatarIcon: 'tabler-globe',
title: 'Social Network',
subtitle: 'Social Channels',
stats: '31.5k',
profitLoss: 8.2,
},
{
avatarIcon: 'tabler-mail',
title: 'Email Newsletter',
subtitle: 'Mail Campaigns',
stats: '893',
profitLoss: 2.4,
},
{
avatarIcon: 'tabler-external-link',
title: 'Referrals',
subtitle: 'Impact Radius Visits',
stats: '342',
profitLoss: -0.4,
},
{
avatarIcon: 'tabler-ad',
title: 'ADVT',
subtitle: 'Google ADVT',
stats: '2.15k',
profitLoss: 9.1,
},
{
avatarIcon: 'tabler-star',
title: 'Other',
subtitle: 'Many Sources',
stats: '12.5k',
profitLoss: 6.2,
},
]
const moreList = [
{
title: 'Refresh',
value: 'refresh',
},
{
title: 'Download',
value: 'Download',
},
{
title: 'View All',
value: 'View All',
},
]
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>Source Visits</VCardTitle>
<VCardSubtitle>
38.4k Visitors
</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="visit in sourceVisits"
:key="visit.title"
>
<template #prepend>
<VAvatar
size="38"
color="secondary"
variant="tonal"
class="me-1"
rounded
>
<VIcon
:icon="visit.avatarIcon"
size="22"
/>
</VAvatar>
</template>
<VListItemTitle class="font-weight-medium me-4">
{{ visit.title }}
</VListItemTitle>
<VListItemSubtitle class="me-4">
{{ visit.subtitle }}
</VListItemSubtitle>
<template #append>
<div class="d-flex align-center gap-x-4">
<div class="text-body-1">
{{ visit.stats }}
</div>
<VChip
label
size="small"
:color="visit.profitLoss > 0 ? 'success' : 'error'"
>
{{ visit.profitLoss > 0 ? '+' : '' }}
{{ visit.profitLoss }}%
</VChip>
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1.5rem;
}
</style>

View File

@@ -0,0 +1,191 @@
<script setup>
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
const vuetifyTheme = useTheme()
const series = [85]
const chartOptions = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
const variableTheme = vuetifyTheme.current.value.variables
return {
labels: ['Completed Task'],
chart: { type: 'radialBar' },
plotOptions: {
radialBar: {
offsetY: 10,
startAngle: -140,
endAngle: 130,
hollow: { size: '65%' },
track: {
background: currentTheme.surface,
strokeWidth: '100%',
},
dataLabels: {
name: {
offsetY: -20,
color: `rgba(${ hexToRgb(currentTheme['on-surface']) },${ variableTheme['disabled-opacity'] })`,
fontSize: '13px',
fontWeight: '400',
fontFamily: 'Public Sans',
},
value: {
offsetY: 10,
color: `rgba(${ hexToRgb(currentTheme['on-background']) },${ variableTheme['high-emphasis-opacity'] })`,
fontSize: '38px',
fontWeight: '500',
fontFamily: 'Public Sans',
},
},
},
},
colors: [currentTheme.primary],
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
shadeIntensity: 0.5,
gradientToColors: [currentTheme.primary],
inverseColors: true,
opacityFrom: 1,
opacityTo: 0.6,
stops: [
30,
70,
100,
],
},
},
stroke: { dashArray: 10 },
grid: {
padding: {
top: -20,
bottom: 5,
},
},
states: {
hover: { filter: { type: 'none' } },
active: { filter: { type: 'none' } },
},
responsive: [{
breakpoint: 960,
options: { chart: { height: 280 } },
}],
}
})
const supportTicket = [
{
avatarColor: 'primary',
avatarIcon: 'tabler-ticket',
title: 'New Tickets',
subtitle: '142',
},
{
avatarColor: 'info',
avatarIcon: 'tabler-check',
title: 'Open Tickets',
subtitle: '28',
},
{
avatarColor: 'warning',
avatarIcon: 'tabler-clock',
title: 'Response Time',
subtitle: '1 Day',
},
]
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>Support Tracker</VCardTitle>
<VCardSubtitle>Last 7 Days</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VRow>
<VCol
cols="12"
lg="4"
md="4"
>
<div class="mb-lg-6 mb-4 mt-2">
<h2 class="text-h2">
164
</h2>
<p class="text-base mb-0">
Total Tickets
</p>
</div>
<VList class="card-list">
<VListItem
v-for="ticket in supportTicket"
:key="ticket.title"
>
<VListItemTitle class="font-weight-medium">
{{ ticket.title }}
</VListItemTitle>
<VListItemSubtitle>
{{ ticket.subtitle }}
</VListItemSubtitle>
<template #prepend>
<VAvatar
rounded
size="34"
:color="ticket.avatarColor"
variant="tonal"
class="me-1"
>
<VIcon
size="22"
:icon="ticket.avatarIcon"
/>
</VAvatar>
</template>
</VListItem>
</VList>
</VCol>
<VCol
cols="12"
lg="8"
md="8"
>
<VueApexCharts
:options="chartOptions"
:series="series"
height="360"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,296 @@
<script setup>
import { useTheme } from 'vuetify'
const vuetifyTheme = useTheme()
const series = [
{
name: 'Earning',
data: [
15,
10,
20,
8,
12,
18,
12,
5,
],
},
{
name: 'Expense',
data: [
-7,
-10,
-7,
-12,
-6,
-9,
-5,
-8,
],
},
]
const chartOptions = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
return {
chart: {
parentHeightOffset: 0,
stacked: true,
type: 'bar',
toolbar: { show: false },
},
tooltip: { enabled: false },
legend: { show: false },
stroke: {
curve: 'smooth',
width: 6,
lineCap: 'round',
colors: [currentTheme.surface],
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '45%',
borderRadius: 8,
borderRadiusApplication: 'around',
borderRadiusWhenStacked: 'all',
},
},
colors: [
'rgba(var(--v-theme-primary),1)',
'rgba(var(--v-theme-secondary),1)',
],
dataLabels: { enabled: false },
grid: {
show: false,
padding: {
top: -40,
bottom: -20,
left: -10,
right: -2,
},
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: { labels: { show: false } },
responsive: [
{
breakpoint: 1600,
options: {
plotOptions: {
bar: {
columnWidth: '50%',
borderRadius: 8,
},
},
},
},
{
breakpoint: 1468,
options: {
plotOptions: {
bar: {
columnWidth: '60%',
borderRadius: 8,
},
},
},
},
{
breakpoint: 1279,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
borderRadius: 8,
},
},
},
},
{
breakpoint: 1197,
options: {
chart: { height: 228 },
plotOptions: {
bar: {
borderRadius: 8,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 912,
options: {
chart: { height: 232 },
plotOptions: {
bar: {
borderRadius: 8,
columnWidth: '55%',
},
},
},
},
{
breakpoint: 725,
options: {
plotOptions: {
bar: {
columnWidth: '70%',
borderRadius: 8,
},
},
},
},
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
borderRadius: 8,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 475,
options: {
plotOptions: {
bar: {
borderRadius: 8,
columnWidth: '50%',
},
},
},
},
{
breakpoint: 381,
options: {
plotOptions: {
bar: {
columnWidth: '60%',
borderRadius: 8,
},
},
},
},
],
states: {
hover: { filter: { type: 'none' } },
active: { filter: { type: 'none' } },
},
}
})
const totalEarnings = [
{
avatar: 'tabler-brand-paypal',
avatarColor: 'primary',
title: 'Total Revenue',
subtitle: 'Client Payment',
earning: '+$126',
},
{
avatar: 'tabler-currency-dollar',
avatarColor: 'secondary',
title: 'Total Sales',
subtitle: 'Total Sales',
earning: '+$98',
},
]
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard>
<VCardItem class="pb-0">
<VCardTitle>Total Earning</VCardTitle>
<div class="d-flex align-center mt-2">
<h2 class="text-h2 me-2">
87%
</h2>
<div class="text-success">
<VIcon
size="20"
icon="tabler-chevron-up"
/>
<span class="text-base">25.8%</span>
</div>
</div>
<template #append>
<div class="mt-n10 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VueApexCharts
:options="chartOptions"
:series="series"
height="191"
class="my-2"
/>
<VList class="card-list">
<VListItem
v-for="earning in totalEarnings"
:key="earning.title"
>
<VListItemTitle class="font-weight-medium">
{{ earning.title }}
</VListItemTitle>
<VListItemSubtitle>
{{ earning.subtitle }}
</VListItemSubtitle>
<template #prepend>
<VAvatar
size="38"
:color="earning.avatarColor"
variant="tonal"
rounded
class="me-1"
>
<VIcon
:icon="earning.avatar"
size="22"
/>
</VAvatar>
</template>
<template #append>
<span class="text-success font-weight-medium">{{ earning.earning }}</span>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,188 @@
<script setup>
import { VIcon } from 'vuetify/components/VIcon'
import sliderBar1 from '@images/illustrations/sidebar-pic-1.png'
import sliderBar2 from '@images/illustrations/sidebar-pic-2.png'
import sliderBar3 from '@images/illustrations/sidebar-pic-3.png'
const websiteAnalytics = [
{
name: 'Traffic',
slideImg: sliderBar1,
data: [
{
number: '1.5k',
text: 'Sessions',
},
{
number: '3.1k',
text: 'Page Views',
},
{
number: '1.2k',
text: 'Leads',
},
{
number: '12%',
text: 'Conversions',
},
],
},
{
name: 'Spending',
slideImg: sliderBar2,
data: [
{
number: '12h',
text: 'Spend',
},
{
number: '182',
text: 'Order Size',
},
{
number: '127',
text: 'Order',
},
{
number: '23k',
text: 'Items',
},
],
},
{
name: 'Revenue Sources',
slideImg: sliderBar3,
data: [
{
number: '268',
text: 'Direct',
},
{
number: '890',
text: 'Organic',
},
{
number: '622',
text: 'Referral',
},
{
number: '1.2k',
text: 'Campaign',
},
],
},
]
</script>
<template>
<VCard
color="primary"
height="260"
>
<VCarousel
cycle
:continuous="false"
:show-arrows="false"
hide-delimiter-background
:delimiter-icon="() => h(VIcon, { icon: 'fa-circle', size: '8' })"
height="260"
class="carousel-delimiter-top-end web-analytics-carousel"
>
<VCarouselItem
v-for="item in websiteAnalytics"
:key="item.name"
>
<VCardText class="position-relative">
<VRow>
<VCol cols="12">
<h5 class="text-h5 text-white">
Website Analytics
</h5>
<p class="text-sm mb-0">
Total 28.5% Conversion Rate
</p>
</VCol>
<VCol
cols="12"
sm="6"
order="2"
order-sm="1"
>
<VRow>
<VCol
cols="12"
class="pb-0 pt-1"
>
<h6 class="text-h6 text-white mb-1 mt-5">
{{ item.name }}
</h6>
</VCol>
<VCol
v-for="d in item.data"
:key="d.number"
cols="6"
class="text-no-wrap pb-2"
>
<VChip
label
variant="flat"
size="default"
color="rgb(var(--v-theme-primary-darken-1))"
class="font-weight-medium text-white rounded me-2 px-2"
style="block-size: 30px;"
>
<span class="text-base">{{ d.number }}</span>
</VChip>
<span class="d-inline-block">{{ d.text }}</span>
</VCol>
</VRow>
</VCol>
<VCol
cols="12"
sm="6"
order="1"
order-sm="2"
class="text-center"
>
<img
:src="item.slideImg"
class="card-website-analytics-img"
style="filter: drop-shadow(0 4px 60px rgba(0, 0, 0, 50%));"
>
</VCol>
</VRow>
</VCardText>
</VCarouselItem>
</VCarousel>
</VCard>
</template>
<style lang="scss">
.card-website-analytics-img {
block-size: 150px;
}
@media screen and (min-width: 600px) {
.card-website-analytics-img {
position: absolute;
margin: auto;
inset-block-end: 1rem;
inset-inline-end: 2.5rem;
}
}
.web-analytics-carousel {
.v-carousel__controls {
.v-carousel__controls__item {
&.v-btn--active {
.v-icon {
opacity: 1 !important;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,134 @@
<script setup>
import bootstrapLogo from '@images/icons/brands/bootstrap-logo.png'
import figmaLogo from '@images/icons/brands/figma-logo.png'
import laravelLogo from '@images/icons/brands/laravel-logo.png'
import reactLogo from '@images/icons/brands/react-logo.png'
import sketchLogo from '@images/icons/brands/sketch-logo.png'
import vuejsLogo from '@images/icons/brands/vuejs-logo.png'
const activeProjects = [
{
avatarImg: laravelLogo,
title: 'Laravel',
subtitle: 'Ecommerce',
stats: '65',
progressColor: 'error',
},
{
avatarImg: figmaLogo,
title: 'Figma',
subtitle: 'App UI Kit',
stats: '86',
progressColor: 'primary',
},
{
avatarImg: vuejsLogo,
title: 'VueJs',
subtitle: 'Calendar App',
stats: '90',
progressColor: 'success',
},
{
avatarImg: reactLogo,
title: 'React',
subtitle: 'Dashboard',
stats: '37',
progressColor: 'info',
},
{
avatarImg: bootstrapLogo,
title: 'Bootstrap',
subtitle: 'Website',
stats: '22',
progressColor: 'primary',
},
{
avatarImg: sketchLogo,
title: 'Sketch',
subtitle: 'Website Design',
stats: '29',
progressColor: 'warning',
},
]
const moreList = [
{
title: 'Refresh',
value: 'refresh',
},
{
title: 'Download',
value: 'Download',
},
{
title: 'View All',
value: 'View All',
},
]
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>Active Projects</VCardTitle>
<VCardSubtitle>
Average 72% completed
</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VList class="card-list">
<VListItem
v-for="project in activeProjects"
:key="project.title"
>
<template #prepend>
<VAvatar
size="34"
rounded
class="me-1"
>
<img :src="project.avatarImg">
</VAvatar>
</template>
<VListItemTitle class="font-weight-medium">
{{ project.title }}
</VListItemTitle>
<VListItemSubtitle class="me-4">
{{ project.subtitle }}
</VListItemSubtitle>
<template #append>
<div class="d-flex align-center gap-x-4">
<div style="inline-size: 4.875rem;">
<VProgressLinear
:model-value="project.stats"
:color="project.progressColor"
height="8"
rounded-bar
rounded
/>
</div>
<span class="text-disabled">{{ project.stats }}%</span>
</div>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,180 @@
<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 pdf from '@images/icons/project-icons/pdf.png'
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard>
<VCardItem>
<template #prepend>
<VIcon
icon="tabler-list-details"
size="20"
color="high-emphasis"
class="me-1"
/>
</template>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
<VCardTitle>Activity Timeline</VCardTitle>
</VCardItem>
<VCardText>
<VTimeline
side="end"
align="start"
line-inset="8"
truncate-line="start"
density="compact"
>
<!-- SECTION Timeline Item: Flight -->
<VTimelineItem
dot-color="primary"
size="x-small"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center gap-2 flex-wrap mb-2">
<span class="app-timeline-title">
12 Invoices have been paid
</span>
<span class="app-timeline-meta">12 min ago</span>
</div>
<!-- 👉 Content -->
<div class="app-timeline-text mt-1">
Invoices have been paid to the company
</div>
<div class="d-inline-flex align-center timeline-chip mt-2">
<img
:src="pdf"
height="20"
class="me-2"
alt="img"
>
<span class="app-timeline-text font-weight-medium">
invoice.pdf
</span>
</div>
</VTimelineItem>
<!-- !SECTION -->
<!-- SECTION Timeline Item: Interview Schedule -->
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap mb-2">
<div class="app-timeline-title">
Client Meeting
</div>
<span class="app-timeline-meta">45 min ago</span>
</div>
<div class="app-timeline-text mt-1">
Project meeting with john @10:15am
</div>
<!-- 👉 Person -->
<div class="d-flex justify-space-between align-center flex-wrap">
<!-- 👉 Avatar & Personal Info -->
<div class="d-flex align-center mt-2">
<VAvatar
size="32"
class="me-2"
:image="avatar1"
/>
<div class="d-flex flex-column">
<p class="text-sm font-weight-medium text-medium-emphasis mb-0">
Lester McCarthy (Client)
</p>
<span class="text-sm">CEO of Pixinvent</span>
</div>
</div>
</div>
</VTimelineItem>
<!-- !SECTION -->
<!-- SECTION Design Review -->
<VTimelineItem
size="x-small"
dot-color="info"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap mb-2">
<span class="app-timeline-title">
Create a new project for client
</span>
<span class="app-timeline-meta">2 Day Ago</span>
</div>
<!-- 👉 Content -->
<p class="app-timeline-text mt-1 mb-2">
6 team members in a project
</p>
<div class="v-avatar-group demo-avatar-group">
<VAvatar :size="40">
<VImg :src="avatar1" />
<VTooltip
activator="parent"
location="top"
>
John Doe
</VTooltip>
</VAvatar>
<VAvatar :size="40">
<VImg :src="avatar2" />
<VTooltip
activator="parent"
location="top"
>
Jennie Obrien
</VTooltip>
</VAvatar>
<VAvatar :size="40">
<VImg :src="avatar3" />
<VTooltip
activator="parent"
location="top"
>
Peter Harper
</VTooltip>
</VAvatar>
<VAvatar
:size="40"
:color="$vuetify.theme.current.dark ? '#3A3B59' : '#F0EFF0'"
>
+3
</VAvatar>
</div>
</VTimelineItem>
<!-- !SECTION -->
</VTimeline>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,164 @@
<script setup>
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
const vuetifyTheme = useTheme()
const series = [
{
name: 'Sales',
data: [
32,
27,
27,
30,
25,
25,
],
},
{
name: 'Visits',
data: [
25,
35,
20,
20,
20,
20,
],
},
]
const chartOptions = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
const variableTheme = vuetifyTheme.current.value.variables
const borderColor = `rgba(${ hexToRgb(String(variableTheme['border-color'])) },${ variableTheme['border-opacity'] })`
const labelColor = `rgba(${ hexToRgb(currentTheme['on-surface']) },${ variableTheme['disabled-opacity'] })`
const legendColor = `rgba(${ hexToRgb(currentTheme['on-background']) },${ variableTheme['medium-emphasis-opacity'] })`
return {
chart: {
type: 'radar',
toolbar: { show: false },
},
plotOptions: {
radar: {
polygons: {
strokeColors: borderColor,
connectorColors: borderColor,
},
},
},
stroke: {
show: false,
width: 0,
},
legend: {
show: true,
fontSize: '13px',
position: 'bottom',
labels: {
colors: legendColor,
useSeriesColors: false,
},
markers: {
height: 12,
width: 12,
offsetX: -8,
},
itemMargin: { horizontal: 10 },
onItemHover: { highlightDataSeries: false },
},
colors: [
currentTheme.primary,
currentTheme.info,
],
fill: {
opacity: [
1,
0.85,
],
},
markers: { size: 0 },
grid: {
show: false,
padding: {
top: 0,
bottom: -5,
},
},
xaxis: {
categories: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
],
labels: {
show: true,
style: {
colors: [
labelColor,
labelColor,
labelColor,
labelColor,
labelColor,
labelColor,
],
fontSize: '13px',
fontFamily: 'Public Sans',
},
},
},
yaxis: {
show: false,
min: 0,
max: 40,
tickAmount: 4,
},
responsive: [{
breakpoint: 769,
options: { chart: { height: 372 } },
}],
}
})
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard>
<VCardItem class="pb-4">
<VCardTitle>Sales</VCardTitle>
<VCardSubtitle>Last 6 Months</VCardSubtitle>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VueApexCharts
:options="chartOptions"
:series="series"
height="290"
/>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,627 @@
<script setup>
import { useTheme } from "vuetify";
import { hexToRgb } from "@layouts/utils";
const vuetifyTheme = useTheme();
const currentTab = ref(0);
const refVueApexChart = ref();
const cardBackgroundStyle = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors;
const primaryColor = currentTheme.primary;
const createGradientColor = (color, opacity = 0.08) => {
if (color.includes("rgba")) {
return color.replace(/[\d\.]+\)$/g, `${opacity})`);
}
if (color.startsWith("#")) {
return `rgba(${hexToRgb(color)}, ${opacity})`;
}
if (color.includes("rgb")) {
return color.replace("rgb", "rgba").replace(")", `, ${opacity})`);
}
return `rgba(${hexToRgb(color)}, ${opacity})`;
};
const gradientColor1 = createGradientColor(primaryColor, 0.12);
const gradientColor2 = createGradientColor(primaryColor, 0.04);
return {
background: `linear-gradient(135deg,
${gradientColor1} 0%,
${gradientColor2} 50%,
${gradientColor1} 100%)`,
};
});
const chartConfigs = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors;
const variableTheme = vuetifyTheme.current.value.variables;
const labelPrimaryColor = `rgba(${hexToRgb(currentTheme.primary)},${
variableTheme["dragged-opacity"]
})`;
const legendColor = `rgba(${hexToRgb(currentTheme["on-background"])},${
variableTheme["high-emphasis-opacity"]
})`;
const borderColor = `rgba(${hexToRgb(
String(variableTheme["border-color"])
)},${variableTheme["border-opacity"]})`;
const labelColor = `rgba(${hexToRgb(currentTheme["on-surface"])},${
variableTheme["disabled-opacity"]
})`;
return [
{
title: "Orders",
icon: "tabler-shopping-cart",
chartOptions: {
chart: {
parentHeightOffset: 0,
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
columnWidth: "32%",
borderRadiusApplication: "end",
borderRadius: 4,
distributed: true,
dataLabels: { position: "top" },
},
},
grid: {
show: false,
padding: {
top: 0,
bottom: 0,
left: -10,
right: -10,
},
},
colors: [
labelPrimaryColor,
labelPrimaryColor,
`rgba(${hexToRgb(currentTheme.primary)}, 1)`,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
],
dataLabels: {
enabled: true,
formatter(val) {
return `${val}k`;
},
offsetY: -25,
style: {
fontSize: "15px",
colors: [legendColor],
fontWeight: "600",
fontFamily: "Public Sans",
},
},
legend: { show: false },
tooltip: { enabled: false },
xaxis: {
categories: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
],
axisBorder: {
show: true,
color: borderColor,
},
axisTicks: { show: false },
labels: {
style: {
colors: labelColor,
fontSize: "13px",
fontFamily: "Public Sans",
},
},
},
yaxis: {
labels: {
offsetX: -15,
formatter(val) {
return `${val / 1}k`;
},
style: {
fontSize: "13px",
colors: labelColor,
fontFamily: "Public Sans",
},
min: 0,
max: 60000,
tickAmount: 6,
},
},
responsive: [
{
breakpoint: 1441,
options: { plotOptions: { bar: { columnWidth: "41%" } } },
},
{
breakpoint: 590,
options: {
plotOptions: { bar: { columnWidth: "61%" } },
yaxis: { labels: { show: false } },
grid: {
padding: {
right: 0,
left: -20,
},
},
dataLabels: {
style: {
fontSize: "12px",
fontWeight: "400",
},
},
},
},
],
},
series: [
{
data: [28, 10, 45, 38, 15, 30, 35, 30, 8],
},
],
},
{
title: "Sales",
icon: "tabler-chart-bar",
chartOptions: {
chart: {
parentHeightOffset: 0,
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
columnWidth: "32%",
borderRadiusApplication: "end",
borderRadius: 4,
distributed: true,
dataLabels: { position: "top" },
},
},
grid: {
show: false,
padding: {
top: 0,
bottom: 0,
left: -10,
right: -10,
},
},
colors: [
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
`rgba(${hexToRgb(currentTheme.primary)}, 1)`,
labelPrimaryColor,
labelPrimaryColor,
],
dataLabels: {
enabled: true,
formatter(val) {
return `${val}k`;
},
offsetY: -25,
style: {
fontSize: "15px",
colors: [legendColor],
fontWeight: "600",
fontFamily: "Public Sans",
},
},
legend: { show: false },
tooltip: { enabled: false },
xaxis: {
categories: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
],
axisBorder: {
show: true,
color: borderColor,
},
axisTicks: { show: false },
labels: {
style: {
colors: labelColor,
fontSize: "13px",
fontFamily: "Public Sans",
},
},
},
yaxis: {
labels: {
offsetX: -15,
formatter(val) {
return `${val / 1}k`;
},
style: {
fontSize: "13px",
colors: labelColor,
fontFamily: "Public Sans",
},
min: 0,
max: 60000,
tickAmount: 6,
},
},
responsive: [
{
breakpoint: 1441,
options: { plotOptions: { bar: { columnWidth: "41%" } } },
},
{
breakpoint: 590,
options: {
plotOptions: { bar: { columnWidth: "61%" } },
grid: { padding: { right: 0 } },
dataLabels: {
style: {
fontSize: "12px",
fontWeight: "400",
},
},
yaxis: { labels: { show: false } },
},
},
],
},
series: [
{
data: [35, 25, 15, 40, 42, 25, 48, 8, 30],
},
],
},
{
title: "Profit",
icon: "tabler-currency-dollar",
chartOptions: {
chart: {
parentHeightOffset: 0,
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
columnWidth: "32%",
borderRadiusApplication: "end",
borderRadius: 4,
distributed: true,
dataLabels: { position: "top" },
},
},
grid: {
show: false,
padding: {
top: 0,
bottom: 0,
left: -10,
right: -10,
},
},
colors: [
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
`rgba(${hexToRgb(currentTheme.primary)}, 1)`,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
],
dataLabels: {
enabled: true,
formatter(val) {
return `${val}k`;
},
offsetY: -25,
style: {
fontSize: "15px",
colors: [legendColor],
fontWeight: "600",
fontFamily: "Public Sans",
},
},
legend: { show: false },
tooltip: { enabled: false },
xaxis: {
categories: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
],
axisBorder: {
show: true,
color: borderColor,
},
axisTicks: { show: false },
labels: {
style: {
colors: labelColor,
fontSize: "13px",
fontFamily: "Public Sans",
},
},
},
yaxis: {
labels: {
offsetX: -15,
formatter(val) {
return `${val / 1}k`;
},
style: {
fontSize: "13px",
colors: labelColor,
fontFamily: "Public Sans",
},
min: 0,
max: 60000,
tickAmount: 6,
},
},
responsive: [
{
breakpoint: 1441,
options: { plotOptions: { bar: { columnWidth: "41%" } } },
},
{
breakpoint: 590,
options: {
plotOptions: { bar: { columnWidth: "61%" } },
grid: { padding: { right: 0 } },
dataLabels: {
style: {
fontSize: "12px",
fontWeight: "400",
},
},
yaxis: { labels: { show: false } },
},
},
],
},
series: [
{
data: [10, 22, 27, 33, 42, 32, 27, 22, 8],
},
],
},
{
title: "Income",
icon: "tabler-chart-pie-2",
chartOptions: {
chart: {
parentHeightOffset: 0,
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
columnWidth: "32%",
borderRadius: 6,
distributed: true,
borderRadiusApplication: "end",
dataLabels: { position: "top" },
},
},
grid: {
show: false,
padding: {
top: 0,
bottom: 0,
left: -10,
right: -10,
},
},
colors: [
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
labelPrimaryColor,
`rgba(${hexToRgb(currentTheme.primary)}, 1)`,
],
dataLabels: {
enabled: true,
formatter(val) {
return `${val}k`;
},
offsetY: -25,
style: {
fontSize: "15px",
colors: [legendColor],
fontWeight: "600",
fontFamily: "Public Sans",
},
},
legend: { show: false },
tooltip: { enabled: false },
xaxis: {
categories: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
],
axisBorder: {
show: true,
color: borderColor,
},
axisTicks: { show: false },
labels: {
style: {
colors: labelColor,
fontSize: "13px",
fontFamily: "Public Sans",
},
},
},
yaxis: {
labels: {
offsetX: -15,
formatter(val) {
return `${val / 1}k`;
},
style: {
fontSize: "13px",
colors: labelColor,
fontFamily: "Public Sans",
},
min: 0,
max: 60000,
tickAmount: 6,
},
},
responsive: [
{
breakpoint: 1441,
options: { plotOptions: { bar: { columnWidth: "41%" } } },
},
{
breakpoint: 590,
options: {
plotOptions: { bar: { columnWidth: "50%" } },
dataLabels: {
style: {
fontSize: "12px",
fontWeight: "400",
},
},
grid: { padding: { right: 0 } },
yaxis: { labels: { show: false } },
},
},
],
},
series: [
{
data: [5, 9, 12, 18, 20, 25, 30, 36, 48],
},
],
},
];
});
const moreList = [
{
title: "View More",
value: "View More",
},
{
title: "Delete",
value: "Delete",
},
];
</script>
<template>
<VCard
title="Earning Reports"
subtitle="Yearly Earnings Overview"
:style="cardBackgroundStyle"
>
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn size="small" :menu-list="moreList" />
</div>
</template>
<VCardText>
<VSlideGroup v-model="currentTab" show-arrows mandatory class="mb-10">
<VSlideGroupItem
v-for="(report, index) in chartConfigs"
:key="report.title"
v-slot="{ isSelected, toggle }"
:value="index"
>
<div
style="block-size: 100px; inline-size: 110px"
:style="
isSelected
? 'border-color:rgb(var(--v-theme-primary)) !important'
: ''
"
:class="isSelected ? 'border' : 'border border-dashed'"
class="d-flex flex-column justify-center align-center cursor-pointer rounded py-4 px-5 me-4"
@click="toggle"
>
<VAvatar
rounded
size="38"
:color="isSelected ? 'primary' : ''"
variant="tonal"
class="mb-2"
>
<VIcon size="22" :icon="report.icon" />
</VAvatar>
<h6 class="text-base font-weight-medium mb-0">
{{ report.title }}
</h6>
</div>
</VSlideGroupItem>
<!-- 👉 slider more -->
<VSlideGroupItem>
<div
style="block-size: 100px; inline-size: 110px"
class="d-flex flex-column justify-center align-center rounded border border-dashed py-4 px-5"
>
<VAvatar rounded size="38" variant="tonal">
<VIcon size="22" icon="tabler-plus" />
</VAvatar>
</div>
</VSlideGroupItem>
</VSlideGroup>
<VueApexCharts
ref="refVueApexChart"
:key="currentTab"
:options="chartConfigs[Number(currentTab)].chartOptions"
:series="chartConfigs[Number(currentTab)].series"
height="230"
class="mt-3"
/>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,186 @@
<script setup>
const series = [{
name: '2020',
data: [
60,
50,
20,
45,
50,
30,
70,
],
}]
const chartOptions = computed(() => {
return {
chart: {
height: 90,
parentHeightOffset: 0,
type: 'bar',
toolbar: { show: false },
},
tooltip: { enabled: false },
plotOptions: {
bar: {
barHeight: '100%',
columnWidth: '30%',
startingShape: 'rounded',
endingShape: 'rounded',
borderRadius: 4,
colors: {
backgroundBarColors: [
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
'rgba(var(--v-track-bg))',
],
backgroundBarRadius: 4,
},
},
},
colors: ['rgba(var(--v-theme-primary),1)'],
grid: {
show: false,
padding: {
top: -30,
left: -16,
bottom: 0,
right: -6,
},
},
dataLabels: { enabled: false },
legend: { show: false },
xaxis: {
categories: [
'M',
'T',
'W',
'T',
'F',
'S',
'S',
],
axisBorder: { show: false },
axisTicks: { show: false },
labels: { show: false },
},
yaxis: { labels: { show: false } },
responsive: [
{
breakpoint: 1441,
options: {
plotOptions: {
bar: {
columnWidth: '30%',
borderRadius: 4,
},
},
},
},
{
breakpoint: 1368,
options: { plotOptions: { bar: { columnWidth: '48%' } } },
},
{
breakpoint: 1264,
options: {
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '30%',
colors: { backgroundBarRadius: 6 },
},
},
},
},
{
breakpoint: 960,
options: {
plotOptions: {
bar: {
columnWidth: '15%',
borderRadius: 4,
},
},
},
},
{
breakpoint: 883,
options: { plotOptions: { bar: { columnWidth: '20%' } } },
},
{
breakpoint: 768,
options: { plotOptions: { bar: { columnWidth: '25%' } } },
},
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
columnWidth: '15%',
borderRadius: 4,
},
colors: { backgroundBarRadius: 9 },
},
},
},
{
breakpoint: 479,
options: {
plotOptions: {
bar: { borderRadius: 4 },
colors: { backgroundBarRadius: 9 },
},
grid: {
padding: {
right: -15,
left: -15,
},
},
},
},
{
breakpoint: 400,
options: { plotOptions: { bar: { borderRadius: 4 } } },
},
],
}
})
</script>
<template>
<VCard>
<VCardItem class="pb-3">
<VCardTitle>Orders</VCardTitle>
<VCardSubtitle>Last Week</VCardSubtitle>
</VCardItem>
<VCardText>
<VueApexCharts
:options="chartOptions"
:series="series"
:height="62"
/>
<div class="d-flex align-center justify-space-between gap-x-2 mt-3">
<h4 class="text-h4 text-center">
124k
</h4>
<div class="text-sm text-success">
+12.6%
</div>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,169 @@
<script setup>
import { useTheme } from 'vuetify'
import { prefixWithPlus } from '@core/utils/formatters'
const vuetifyTheme = useTheme()
const series = [{
data: [
2000,
2000,
4000,
4000,
3050,
3050,
2000,
2000,
3050,
3050,
4700,
4700,
2750,
2750,
5700,
5700,
],
}]
const chartOptions = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
return {
chart: {
type: 'area',
toolbar: false,
},
markers: { strokeColor: 'transparent' },
dataLabels: { enabled: false },
grid: {
show: false,
padding: {
left: -10,
right: -5,
top: -40,
},
},
stroke: {
width: 3,
curve: 'straight',
},
colors: [currentTheme.warning],
fill: {
type: 'gradient',
gradient: {
opacityFrom: 0.6,
opacityTo: 0.15,
stops: [
0,
95,
100,
],
},
},
xaxis: {
labels: { show: false },
axisBorder: { show: false },
axisTicks: { show: false },
lines: { show: false },
},
yaxis: {
labels: { show: false },
min: 1000,
max: 6000,
tickAmount: 5,
},
tooltip: { enabled: false },
}
})
const projectStatus = [
{
title: 'Donates',
amount: '$756.26',
lossProfit: -139.34,
},
{
title: 'Podcasts',
amount: '$2,207.03',
lossProfit: +576.24,
},
]
const moreList = [
{
title: 'View More',
value: 'View More',
},
{
title: 'Delete',
value: 'Delete',
},
]
</script>
<template>
<VCard title="Project Status">
<template #append>
<div class="mt-n4 me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
<VCardText>
<VList class="card-list mb-6">
<VListItem>
<VListItemTitle class="font-weight-medium">
$4,3742
</VListItemTitle>
<template #prepend>
<VAvatar
color="warning"
variant="tonal"
rounded
icon="tabler-currency-dollar"
/>
</template>
<VListItemSubtitle>
Your Earnings
</VListItemSubtitle>
<template #append>
<span class="text-success font-weight-medium">+10.2%</span>
</template>
</VListItem>
</VList>
<VueApexCharts
:options="chartOptions"
:series="series"
height="208"
/>
<VList class="card-list">
<VListItem
v-for="status in projectStatus"
:key="status.title"
>
<VListItemTitle class="font-weight-medium">
{{ status.title }}
</VListItemTitle>
<template #append>
<span class="me-4 text-medium-emphasis">{{ status.amount }}</span>
<span :class="status.lossProfit > 0 ? 'text-success' : 'text-error'">{{ prefixWithPlus(status.lossProfit) }}</span>
</template>
</VListItem>
</VList>
</VCardText>
</VCard>
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,168 @@
<script setup>
import aeIcon from '@images/icons/payments/ae-icon.png'
import mastercardIcon from '@images/icons/payments/mastercard-icon.png'
import visaIcon from '@images/icons/payments/visa-icon.png'
const lastTransitions = [
{
cardImg: visaIcon,
lastDigit: '*4230',
cardType: 'Credit',
sentDate: '17 Mar 2022',
status: 'Verified',
trend: '+$1,678',
},
{
cardImg: mastercardIcon,
lastDigit: '*5578',
cardType: 'Credit',
sentDate: '12 Feb 2022',
status: 'Rejected',
trend: '-$839',
},
{
cardImg: aeIcon,
lastDigit: '*4567',
cardType: 'Credit',
sentDate: '28 Feb 2022',
status: 'Verified',
trend: '+$435',
},
{
cardImg: visaIcon,
lastDigit: '*5699',
cardType: 'Credit',
sentDate: '8 Jan 2022',
status: 'Pending',
trend: '+$2,345',
},
{
cardImg: visaIcon,
lastDigit: '*5699',
cardType: 'Credit',
sentDate: '8 Jan 2022',
status: 'Rejected',
trend: '-$234',
},
]
const resolveStatus = {
Verified: 'success',
Rejected: 'error',
Pending: 'secondary',
}
const moreList = [
{
title: 'Refresh',
value: 'refresh',
},
{
title: 'Download',
value: 'Download',
},
{
title: 'View All',
value: 'View All',
},
]
const getPaddingStyle = index => index ? 'padding-block-end: 1.25rem;' : 'padding-block: 1.25rem;'
</script>
<template>
<VCard title="Last Transaction">
<template #append>
<div class="me-n2">
<MoreBtn
size="small"
:menu-list="moreList"
/>
</div>
</template>
<VDivider />
<VTable class="text-no-wrap transaction-table">
<thead>
<tr>
<th>CARD</th>
<th>DATE</th>
<th>STATUS</th>
<th>
TREND
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(transition, index) in lastTransitions"
:key="index"
>
<td
:style="getPaddingStyle(index)"
style="padding-inline-end: 1.5rem;"
>
<div class="d-flex align-center">
<div class="me-4">
<VImg
:src="transition.cardImg"
width="50"
/>
</div>
<div>
<p class="text-base mb-0 text-high-emphasis">
{{ transition.lastDigit }}
</p>
<p class="text-sm mb-0">
{{ transition.cardType }}
</p>
</div>
</div>
</td>
<td
:style="getPaddingStyle(index)"
style="padding-inline-end: 1.5rem;"
>
<p class="text-high-emphasis text-base mb-0">
Sent
</p>
<div class="text-sm">
{{ transition.sentDate }}
</div>
</td>
<td
:style="getPaddingStyle(index)"
style="padding-inline-end: 1.5rem;"
>
<VChip
label
:color="resolveStatus[transition.status]"
size="small"
>
{{ transition.status }}
</VChip>
</td>
<td
:style="getPaddingStyle(index)"
style="padding-inline-end: 1.5rem;"
align="right"
>
<div class="text-high-emphasis text-base">
{{ transition.trend }}
</div>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</template>
<style lang="scss">
.transaction-table {
&.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > td,
&.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > th {
border-block-end: none !important;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More