Initial commit
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
84
resources/js/views/apps/academy/AcademyCardTopCourses.vue
Normal file
84
resources/js/views/apps/academy/AcademyCardTopCourses.vue
Normal 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>
|
||||
192
resources/js/views/apps/academy/AcademyCourseTable.vue
Normal file
192
resources/js/views/apps/academy/AcademyCourseTable.vue
Normal 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>
|
||||
239
resources/js/views/apps/academy/AcademyMyCourses.vue
Normal file
239
resources/js/views/apps/academy/AcademyMyCourses.vue
Normal 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>
|
||||
203
resources/js/views/apps/academy/AcademyTopicYouAreInterested.vue
Normal file
203
resources/js/views/apps/academy/AcademyTopicYouAreInterested.vue
Normal 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>
|
||||
54
resources/js/views/apps/academy/AcademyUpcomingWebinar.vue
Normal file
54
resources/js/views/apps/academy/AcademyUpcomingWebinar.vue
Normal 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>
|
||||
341
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal file
341
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal 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>
|
||||
1
resources/js/views/apps/calendar/types.js
Normal file
1
resources/js/views/apps/calendar/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
302
resources/js/views/apps/calendar/useCalendar.js
Normal file
302
resources/js/views/apps/calendar/useCalendar.js
Normal 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,
|
||||
}
|
||||
}
|
||||
59
resources/js/views/apps/calendar/useCalendarStore.js
Normal file
59
resources/js/views/apps/calendar/useCalendarStore.js
Normal 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',
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
110
resources/js/views/apps/chat/ChatContact.vue
Normal file
110
resources/js/views/apps/chat/ChatContact.vue
Normal 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>
|
||||
140
resources/js/views/apps/chat/ChatLeftSidebarContent.vue
Normal file
140
resources/js/views/apps/chat/ChatLeftSidebarContent.vue
Normal 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>
|
||||
142
resources/js/views/apps/chat/ChatLog.vue
Normal file
142
resources/js/views/apps/chat/ChatLog.vue
Normal 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>
|
||||
214
resources/js/views/apps/chat/ChatUserProfileSidebarContent.vue
Normal file
214
resources/js/views/apps/chat/ChatUserProfileSidebarContent.vue
Normal 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>
|
||||
16
resources/js/views/apps/chat/useChat.js
Normal file
16
resources/js/views/apps/chat/useChat.js
Normal 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,
|
||||
}
|
||||
}
|
||||
80
resources/js/views/apps/chat/useChatStore.js
Normal file
80
resources/js/views/apps/chat/useChatStore.js
Normal 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
|
||||
},
|
||||
},
|
||||
})
|
||||
281
resources/js/views/apps/ecommerce/ECommerceAddCategoryDrawer.vue
Normal file
281
resources/js/views/apps/ecommerce/ECommerceAddCategoryDrawer.vue
Normal 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>
|
||||
211
resources/js/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue
Normal file
211
resources/js/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
141
resources/js/views/apps/ecommerce/settings/SettingsCheckout.vue
Normal file
141
resources/js/views/apps/ecommerce/settings/SettingsCheckout.vue
Normal 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>
|
||||
119
resources/js/views/apps/ecommerce/settings/SettingsLocations.vue
Normal file
119
resources/js/views/apps/ecommerce/settings/SettingsLocations.vue
Normal 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>
|
||||
@@ -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>
|
||||
158
resources/js/views/apps/ecommerce/settings/SettingsPayment.vue
Normal file
158
resources/js/views/apps/ecommerce/settings/SettingsPayment.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
1
resources/js/views/apps/ecommerce/types.js
Normal file
1
resources/js/views/apps/ecommerce/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
246
resources/js/views/apps/email/ComposeDialog.vue
Normal file
246
resources/js/views/apps/email/ComposeDialog.vue
Normal 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>
|
||||
261
resources/js/views/apps/email/EmailLeftSidebarContent.vue
Normal file
261
resources/js/views/apps/email/EmailLeftSidebarContent.vue
Normal 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>
|
||||
432
resources/js/views/apps/email/EmailView.vue
Normal file
432
resources/js/views/apps/email/EmailView.vue
Normal 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>
|
||||
94
resources/js/views/apps/email/useEmail.js
Normal file
94
resources/js/views/apps/email/useEmail.js
Normal 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,
|
||||
}
|
||||
}
|
||||
131
resources/js/views/apps/invoice/InvoiceAddPaymentDrawer.vue
Normal file
131
resources/js/views/apps/invoice/InvoiceAddPaymentDrawer.vue
Normal 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>
|
||||
335
resources/js/views/apps/invoice/InvoiceEditable.vue
Normal file
335
resources/js/views/apps/invoice/InvoiceEditable.vue
Normal 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>
|
||||
212
resources/js/views/apps/invoice/InvoiceProductEdit.vue
Normal file
212
resources/js/views/apps/invoice/InvoiceProductEdit.vue
Normal 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>
|
||||
141
resources/js/views/apps/invoice/InvoiceSendInvoiceDrawer.vue
Normal file
141
resources/js/views/apps/invoice/InvoiceSendInvoiceDrawer.vue
Normal 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>
|
||||
1
resources/js/views/apps/invoice/types.js
Normal file
1
resources/js/views/apps/invoice/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
237
resources/js/views/apps/kanban/KanbanBoard.vue
Normal file
237
resources/js/views/apps/kanban/KanbanBoard.vue
Normal 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>
|
||||
390
resources/js/views/apps/kanban/KanbanBoardEditDrawer.vue
Normal file
390
resources/js/views/apps/kanban/KanbanBoardEditDrawer.vue
Normal 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>
|
||||
171
resources/js/views/apps/kanban/KanbanCard.vue
Normal file
171
resources/js/views/apps/kanban/KanbanCard.vue
Normal 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>
|
||||
312
resources/js/views/apps/kanban/KanbanItems.vue
Normal file
312
resources/js/views/apps/kanban/KanbanItems.vue
Normal 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>
|
||||
116
resources/js/views/apps/logistics/LogisticsCardStatistics.vue
Normal file
116
resources/js/views/apps/logistics/LogisticsCardStatistics.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
347
resources/js/views/apps/logistics/LogisticsOrderByCountries.vue
Normal file
347
resources/js/views/apps/logistics/LogisticsOrderByCountries.vue
Normal 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>
|
||||
153
resources/js/views/apps/logistics/LogisticsOverviewTable.vue
Normal file
153
resources/js/views/apps/logistics/LogisticsOverviewTable.vue
Normal 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>
|
||||
@@ -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>
|
||||
188
resources/js/views/apps/logistics/LogisticsVehicleOverview.vue
Normal file
188
resources/js/views/apps/logistics/LogisticsVehicleOverview.vue
Normal 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>
|
||||
330
resources/js/views/apps/roles/RoleCards.vue
Normal file
330
resources/js/views/apps/roles/RoleCards.vue
Normal 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>
|
||||
364
resources/js/views/apps/roles/UserList.vue
Normal file
364
resources/js/views/apps/roles/UserList.vue
Normal 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>
|
||||
213
resources/js/views/apps/user/list/AddNewUserDrawer.vue
Normal file
213
resources/js/views/apps/user/list/AddNewUserDrawer.vue
Normal 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>
|
||||
1
resources/js/views/apps/user/types.js
Normal file
1
resources/js/views/apps/user/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
367
resources/js/views/apps/user/view/UserBioPanel.vue
Normal file
367
resources/js/views/apps/user/view/UserBioPanel.vue
Normal 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>
|
||||
261
resources/js/views/apps/user/view/UserInvoiceTable.vue
Normal file
261
resources/js/views/apps/user/view/UserInvoiceTable.vue
Normal 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>
|
||||
424
resources/js/views/apps/user/view/UserTabAccount.vue
Normal file
424
resources/js/views/apps/user/view/UserTabAccount.vue
Normal 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>
|
||||
425
resources/js/views/apps/user/view/UserTabBillingsPlans.vue
Normal file
425
resources/js/views/apps/user/view/UserTabBillingsPlans.vue
Normal 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>
|
||||
181
resources/js/views/apps/user/view/UserTabConnections.vue
Normal file
181
resources/js/views/apps/user/view/UserTabConnections.vue
Normal 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>
|
||||
89
resources/js/views/apps/user/view/UserTabNotifications.vue
Normal file
89
resources/js/views/apps/user/view/UserTabNotifications.vue
Normal 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>
|
||||
188
resources/js/views/apps/user/view/UserTabSecurity.vue
Normal file
188
resources/js/views/apps/user/view/UserTabSecurity.vue
Normal 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>
|
||||
73
resources/js/views/charts/apex-chart/ApexChartAreaChart.vue
Normal file
73
resources/js/views/charts/apex-chart/ApexChartAreaChart.vue
Normal 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>
|
||||
36
resources/js/views/charts/apex-chart/ApexChartBalance.vue
Normal file
36
resources/js/views/charts/apex-chart/ApexChartBalance.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
22
resources/js/views/charts/apex-chart/ApexChartStatistics.vue
Normal file
22
resources/js/views/charts/apex-chart/ApexChartStatistics.vue
Normal 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>
|
||||
147
resources/js/views/charts/apex-chart/ApexChartStocksPrices.vue
Normal file
147
resources/js/views/charts/apex-chart/ApexChartStocksPrices.vue
Normal 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>
|
||||
65
resources/js/views/charts/chartjs/ChartJsBarChart.vue
Normal file
65
resources/js/views/charts/chartjs/ChartJsBarChart.vue
Normal 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>
|
||||
173
resources/js/views/charts/chartjs/ChartJsBubbleChart.vue
Normal file
173
resources/js/views/charts/chartjs/ChartJsBubbleChart.vue
Normal 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>
|
||||
@@ -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>
|
||||
139
resources/js/views/charts/chartjs/ChartJsLineAreaChart.vue
Normal file
139
resources/js/views/charts/chartjs/ChartJsLineAreaChart.vue
Normal 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>
|
||||
139
resources/js/views/charts/chartjs/ChartJsLineChart.vue
Normal file
139
resources/js/views/charts/chartjs/ChartJsLineChart.vue
Normal 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>
|
||||
54
resources/js/views/charts/chartjs/ChartJsPolarAreaChart.vue
Normal file
54
resources/js/views/charts/chartjs/ChartJsPolarAreaChart.vue
Normal 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>
|
||||
61
resources/js/views/charts/chartjs/ChartJsRadarChart.vue
Normal file
61
resources/js/views/charts/chartjs/ChartJsRadarChart.vue
Normal 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>
|
||||
238
resources/js/views/charts/chartjs/ChartJsScatterChart.vue
Normal file
238
resources/js/views/charts/chartjs/ChartJsScatterChart.vue
Normal 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>
|
||||
1
resources/js/views/charts/chartjs/types.js
Normal file
1
resources/js/views/charts/chartjs/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
134
resources/js/views/dashboards/crm/CrmActiveProject.vue
Normal file
134
resources/js/views/dashboards/crm/CrmActiveProject.vue
Normal 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>
|
||||
180
resources/js/views/dashboards/crm/CrmActivityTimeline.vue
Normal file
180
resources/js/views/dashboards/crm/CrmActivityTimeline.vue
Normal 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>
|
||||
164
resources/js/views/dashboards/crm/CrmAnalyticsSales.vue
Normal file
164
resources/js/views/dashboards/crm/CrmAnalyticsSales.vue
Normal 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>
|
||||
@@ -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>
|
||||
186
resources/js/views/dashboards/crm/CrmOrderBarChart.vue
Normal file
186
resources/js/views/dashboards/crm/CrmOrderBarChart.vue
Normal 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>
|
||||
169
resources/js/views/dashboards/crm/CrmProjectStatus.vue
Normal file
169
resources/js/views/dashboards/crm/CrmProjectStatus.vue
Normal 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>
|
||||
168
resources/js/views/dashboards/crm/CrmRecentTransactions.vue
Normal file
168
resources/js/views/dashboards/crm/CrmRecentTransactions.vue
Normal 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
Reference in New Issue
Block a user