first commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
const assignmentData = [
|
||||
{
|
||||
title: 'User Experience Design',
|
||||
tasks: 120,
|
||||
progress: 72,
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Basic fundamentals',
|
||||
tasks: 32,
|
||||
progress: 48,
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'React Native components',
|
||||
tasks: 182,
|
||||
progress: 15,
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Basic of music theory',
|
||||
tasks: 56,
|
||||
progress: 24,
|
||||
color: 'info',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="Assignment progress">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="assignment in assignmentData"
|
||||
:key="assignment.title"
|
||||
>
|
||||
<template #prepend>
|
||||
<VProgressCircular
|
||||
v-model="assignment.progress"
|
||||
:size="54"
|
||||
class="me-4"
|
||||
:color="assignment.color"
|
||||
>
|
||||
<h6 class="text-h6">
|
||||
{{ assignment.progress }}%
|
||||
</h6>
|
||||
</VProgressCircular>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="text-h6 me-4 mb-2 text-truncate">
|
||||
{{ assignment.title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VListItemSubtitle>{{ assignment.tasks }} Tasks</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
class="rounded"
|
||||
size="34"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-arrow-right-s-line"
|
||||
size="20"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 1.5rem;
|
||||
}
|
||||
</style>
|
238
resources/js/views/apps/academy/AcademyCardInterestedTopics.vue
Normal file
238
resources/js/views/apps/academy/AcademyCardInterestedTopics.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<script setup>
|
||||
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
|
||||
|
||||
// Topics Charts config
|
||||
const topicsChartConfig = {
|
||||
chart: {
|
||||
height: 270,
|
||||
type: 'bar',
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: '70%',
|
||||
distributed: true,
|
||||
borderRadius: 7,
|
||||
borderRadiusApplication: 'end',
|
||||
},
|
||||
},
|
||||
colors: [
|
||||
'rgba(var(--v-theme-primary),1)',
|
||||
'#16B1FF',
|
||||
'#56CA00',
|
||||
'#8A8D93',
|
||||
'#FF4C51',
|
||||
'#FFB400',
|
||||
],
|
||||
grid: {
|
||||
borderColor,
|
||||
strokeDashArray: 10,
|
||||
xaxis: { lines: { show: true } },
|
||||
yaxis: { lines: { show: false } },
|
||||
padding: {
|
||||
top: -35,
|
||||
bottom: -12,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
colors: ['#fff'],
|
||||
fontWeight: 200,
|
||||
fontSize: '13px',
|
||||
},
|
||||
offsetX: 0,
|
||||
dropShadow: { enabled: false },
|
||||
formatter(val, opt) {
|
||||
return topicsChartConfig.labels[opt.dataPointIndex]
|
||||
},
|
||||
},
|
||||
labels: [
|
||||
'UI Design',
|
||||
'UX Design',
|
||||
'Music',
|
||||
'Animation',
|
||||
'Vue',
|
||||
'SEO',
|
||||
],
|
||||
xaxis: {
|
||||
categories: [
|
||||
'6',
|
||||
'5',
|
||||
'4',
|
||||
'3',
|
||||
'2',
|
||||
'1',
|
||||
],
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { show: false },
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
|
||||
fontSize: '13px',
|
||||
},
|
||||
formatter(val) {
|
||||
return `${ val }%`
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
max: 35,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
|
||||
fontSize: '13px',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
style: { fontSize: '12px' },
|
||||
onDatasetHover: { highlightDataSeries: false },
|
||||
},
|
||||
legend: { show: false },
|
||||
}
|
||||
|
||||
const topicsChartSeries = [{
|
||||
data: [
|
||||
35,
|
||||
20,
|
||||
14,
|
||||
12,
|
||||
10,
|
||||
9,
|
||||
],
|
||||
}]
|
||||
|
||||
const topicsData = [
|
||||
{
|
||||
title: 'UI Design',
|
||||
value: 35,
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'UX Design',
|
||||
value: 20,
|
||||
color: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Music',
|
||||
value: 14,
|
||||
color: 'success',
|
||||
},
|
||||
]
|
||||
|
||||
const moreTopics = [
|
||||
{
|
||||
title: 'Animation',
|
||||
value: 12,
|
||||
color: 'secondary',
|
||||
},
|
||||
{
|
||||
title: 'Vue',
|
||||
value: 10,
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
title: 'SEO',
|
||||
value: 9,
|
||||
color: 'warning',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="topicCard">
|
||||
<VCardItem title="Topic you are interested in">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<VueApexCharts
|
||||
type="bar"
|
||||
height="300"
|
||||
:options="topicsChartConfig"
|
||||
:series="topicsChartSeries"
|
||||
class="mb-md-0 mb-6"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol class="d-flex justify-space-around align-center">
|
||||
<div class="d-flex flex-column gap-y-12">
|
||||
<div
|
||||
v-for="topic in topicsData"
|
||||
:key="topic.title"
|
||||
class="d-flex gap-x-2"
|
||||
>
|
||||
<VBadge
|
||||
inline
|
||||
dot
|
||||
:color="topic.color"
|
||||
class="mt-1"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
class="text-body-1"
|
||||
style="min-inline-size: 90px;"
|
||||
>
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<h5 class="text-h5">
|
||||
{{ topic.value }}%
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-y-12">
|
||||
<div
|
||||
v-for="topic in moreTopics"
|
||||
:key="topic.title"
|
||||
class="d-flex gap-x-2"
|
||||
>
|
||||
<VBadge
|
||||
inline
|
||||
dot
|
||||
:color="topic.color"
|
||||
class="mt-1"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
class="text-body-1"
|
||||
style="min-inline-size: 90px;"
|
||||
>
|
||||
{{ topic.title }}
|
||||
</div>
|
||||
<h5 class="text-h5">
|
||||
{{ topic.value }}%
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
|
||||
.topicCard{
|
||||
.v-badge.v-badge--dot{
|
||||
.v-badge__badge{
|
||||
border-radius: 6px;
|
||||
block-size: 12px;
|
||||
inline-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="Popular Instructors">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<div class="text-overline d-flex justify-space-between px-5 py-4">
|
||||
<div>instructors</div>
|
||||
<div>Courses</div>
|
||||
</div>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="instructor in [
|
||||
{ name: 'Jordan Stevenson', profession: 'Business Intelligence', totalCourses: 33, avatar: avatar1 },
|
||||
{ name: 'Bentlee Emblin', profession: 'Digital Marketing', totalCourses: 52, avatar: avatar2 },
|
||||
{ name: 'Benedetto Rossiter', profession: 'UI/UX Design', totalCourses: 12, avatar: avatar3 },
|
||||
{ name: 'Beverlie Krabbe', profession: 'Vue', totalCourses: 8, avatar: avatar4 },
|
||||
]"
|
||||
:key="instructor.name"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="34"
|
||||
:image="instructor.avatar"
|
||||
/>
|
||||
</template>
|
||||
<h6 class="text-h6">
|
||||
{{ instructor.name }}
|
||||
</h6>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ instructor.profession }}
|
||||
</div>
|
||||
|
||||
<template #append>
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
{{ instructor.totalCourses }}
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list{
|
||||
--v-card-list-gap: 16px;
|
||||
|
||||
}
|
||||
</style>
|
82
resources/js/views/apps/academy/AcademyCardTopCourses.vue
Normal file
82
resources/js/views/apps/academy/AcademyCardTopCourses.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
const coursesData = [
|
||||
{
|
||||
title: 'Videography Basic Design Course',
|
||||
views: '1.2k',
|
||||
icon: 'ri-video-download-line',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Basic Front-end Development Course',
|
||||
views: '834',
|
||||
icon: 'ri-code-view',
|
||||
color: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Basic Fundamentals of Photography',
|
||||
views: '3.7k',
|
||||
icon: 'ri-image-2-line',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Advance Dribble Base Visual Design',
|
||||
views: '2.5k',
|
||||
icon: 'ri-palette-line',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Your First Singing Lesson',
|
||||
views: '948',
|
||||
icon: 'ri-music-2-line',
|
||||
color: 'error',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="Top Courses">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="(course, index) in coursesData"
|
||||
:key="index"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
rounded
|
||||
variant="tonal"
|
||||
:color="course.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="course.icon"
|
||||
size="24"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<div class="text-h6 clamp-text text-wrap me-4">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<VChip
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
size="small"
|
||||
>
|
||||
{{ course.views }} Views
|
||||
</VChip>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
219
resources/js/views/apps/academy/AcademyCourseTable.vue
Normal file
219
resources/js/views/apps/academy/AcademyCourseTable.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<script setup>
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(5)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Course Name',
|
||||
key: 'courseName',
|
||||
},
|
||||
{
|
||||
title: 'Time',
|
||||
key: 'time',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Progress',
|
||||
key: 'progress',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const { data: courseData } = await useApi(createUrl('/apps/academy/courses', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const courses = computed(() => courseData.value.courses)
|
||||
const totalCourse = computed(() => courseData.value.total)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-wrap justify-space-between align-center gap-4">
|
||||
<h5 class="text-h5">
|
||||
Courses you are taking
|
||||
</h5>
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Course"
|
||||
density="compact"
|
||||
style="max-inline-size: 300px; min-inline-size: 200px;"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="[
|
||||
{ value: 5, title: '5' },
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 20, title: '20' },
|
||||
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
|
||||
]"
|
||||
:headers="headers"
|
||||
:items="courses"
|
||||
item-value="id"
|
||||
:items-length="totalCourse"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.courseName="{ item }">
|
||||
<div class="d-flex align-center gap-x-4 py-2">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
:color="item.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="item.logo"
|
||||
size="28"
|
||||
/>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-academy-course-details' }"
|
||||
class="d-inline-block text-h6 mb-1"
|
||||
>
|
||||
{{ item.courseTitle }}
|
||||
</RouterLink>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="22"
|
||||
:image="item.image"
|
||||
class="me-2"
|
||||
/>
|
||||
<div class="text-body-2 text-high-emphasis">
|
||||
{{ item.user }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.time="{ item }">
|
||||
<h6 class="text-h6">
|
||||
{{ item.time }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<template #item.progress="{ item }">
|
||||
<div
|
||||
class="d-flex align-center gap-x-4 mb-2"
|
||||
style="inline-size: 15.625rem;"
|
||||
>
|
||||
<div class="text-no-wrap text-h6">
|
||||
{{ Math.floor((item.completedTasks / item.totalTasks) * 100) }}%
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<VProgressLinear
|
||||
color="primary"
|
||||
height="8"
|
||||
:model-value="Math.floor((item.completedTasks / item.totalTasks) * 100)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
{{ item.completedTasks }}/{{ item.totalTasks }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<div class="d-flex justify-space-between gap-x-4">
|
||||
<div class="d-flex gap-x-2 align-center">
|
||||
<VIcon
|
||||
icon="ri-group-line"
|
||||
color="primary"
|
||||
size="24"
|
||||
/>
|
||||
<span class="text-body-1">
|
||||
{{ item.userCount }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-x-2 align-center">
|
||||
<VIcon
|
||||
icon="ri-computer-line"
|
||||
color="info"
|
||||
size="24"
|
||||
/>
|
||||
<span class="text-body-1">{{ item.note }}</span>
|
||||
</div>
|
||||
<div class="d-flex gap-x-2 align-center">
|
||||
<VIcon
|
||||
icon="ri-video-upload-line"
|
||||
color="error"
|
||||
size="24"
|
||||
/>
|
||||
<span class="text-body-1">{{ item.view }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalCourse) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl text-high-emphasis"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl text-high-emphasis"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalCourse / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalCourse / itemsPerPage) ? page = Math.ceil(totalCourse / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</template>
|
234
resources/js/views/apps/academy/AcademyMyCourses.vue
Normal file
234
resources/js/views/apps/academy/AcademyMyCourses.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
searchQuery: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const itemsPerPage = ref(6)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
const hideCompleted = ref(true)
|
||||
const label = ref('All Courses')
|
||||
|
||||
const { data: coursesData } = await useApi(createUrl('/apps/academy/courses', {
|
||||
query: {
|
||||
q: () => props.searchQuery,
|
||||
hideCompleted,
|
||||
label,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const courses = computed(() => coursesData.value.courses)
|
||||
const totalCourse = computed(() => coursesData.value.total)
|
||||
|
||||
watch([
|
||||
hideCompleted,
|
||||
label,
|
||||
() => props.searchQuery,
|
||||
], () => {
|
||||
page.value = 1
|
||||
})
|
||||
|
||||
const resolveChipColor = tags => {
|
||||
if (tags === 'Web')
|
||||
return 'primary'
|
||||
if (tags === 'Art')
|
||||
return 'success'
|
||||
if (tags === 'UI/UX')
|
||||
return 'error'
|
||||
if (tags === 'Psychology')
|
||||
return 'warning'
|
||||
if (tags === 'Design')
|
||||
return 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<!-- 👉 Header -->
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-4 mb-6">
|
||||
<div>
|
||||
<h5 class="text-h5">
|
||||
My Courses
|
||||
</h5>
|
||||
<div class="text-body-1">
|
||||
Total 6 course you have purchased
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-center gap-y-4 gap-x-6">
|
||||
<VSelect
|
||||
v-model="label"
|
||||
density="compact"
|
||||
:items="[
|
||||
{ title: 'Web', value: 'web' },
|
||||
{ title: 'Art', value: 'art' },
|
||||
{ title: 'UI/UX', value: 'ui/ux' },
|
||||
{ title: 'Psychology', value: 'psychology' },
|
||||
{ title: 'Design', value: 'design' },
|
||||
{ title: 'All Courses', value: 'All Courses' },
|
||||
]"
|
||||
style="min-inline-size: 250px;"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="hideCompleted"
|
||||
label="Hide Completed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Course List -->
|
||||
<div class="mb-6">
|
||||
<VRow class="match-height">
|
||||
<template
|
||||
v-for="course in courses"
|
||||
:key="course.id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
>
|
||||
<div class="pa-2">
|
||||
<VImg
|
||||
:src="course.tutorImg"
|
||||
class="cursor-pointer"
|
||||
@click="() => $router.push({ name: 'apps-academy-course-details' })"
|
||||
/>
|
||||
</div>
|
||||
<VCardText class="pt-3">
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<VChip
|
||||
variant="tonal"
|
||||
:color="resolveChipColor(course.tags)"
|
||||
size="small"
|
||||
>
|
||||
{{ course.tags }}
|
||||
</VChip>
|
||||
<div class="d-flex">
|
||||
<h6 class="text-h6 text-medium-emphasis me-1">
|
||||
{{ course.rating }}
|
||||
</h6>
|
||||
<VIcon
|
||||
icon="ri-star-fill"
|
||||
color="warning"
|
||||
class="me-2"
|
||||
/>
|
||||
<div class="text-body-1">
|
||||
({{ course.ratingCount }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="text-h5 mb-1">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-academy-course-details' }"
|
||||
class="course-title"
|
||||
>
|
||||
{{ course.courseTitle }}
|
||||
</RouterLink>
|
||||
</h5>
|
||||
<p>
|
||||
{{ course.desc }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="course.completedTasks !== course.totalTasks"
|
||||
class="d-flex align-center mb-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-time-line"
|
||||
size="20"
|
||||
class="me-1"
|
||||
/>
|
||||
<div class="text-body-1 my-auto">
|
||||
{{ course.time }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mb-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-check-line"
|
||||
color="success"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-success text-body-1">Completed</span>
|
||||
</div>
|
||||
|
||||
<VProgressLinear
|
||||
:model-value="(course.completedTasks / course.totalTasks) * 100"
|
||||
rounded
|
||||
rounded-bar
|
||||
color="primary"
|
||||
height="8"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
class="flex-grow-1"
|
||||
:to="{ name: 'apps-academy-course-details' }"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-refresh-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</template>
|
||||
Start Over
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-if="course.completedTasks !== course.totalTasks"
|
||||
variant="outlined"
|
||||
class="flex-grow-1"
|
||||
:to="{ name: 'apps-academy-course-details' }"
|
||||
>
|
||||
<template #append>
|
||||
<VIcon
|
||||
icon="ri-arrow-right-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</template>
|
||||
Continue
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</div>
|
||||
|
||||
<VPagination
|
||||
v-model="page"
|
||||
rounded
|
||||
color="primary"
|
||||
:length="Math.ceil(totalCourse / itemsPerPage)"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.course-title{
|
||||
&:not(:hover){
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-text-high-emphasis))
|
||||
}
|
||||
}
|
||||
</style>
|
51
resources/js/views/apps/academy/AcademyUpcomingWebinar.vue
Normal file
51
resources/js/views/apps/academy/AcademyUpcomingWebinar.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import girlWithLaptop from '@images/pages/pose-fs-9.png'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-center align-start pb-0 px-3 pt-3 mb-6 bg-light-primary rounded">
|
||||
<VImg
|
||||
:src="girlWithLaptop"
|
||||
width="145"
|
||||
height="140"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-1">
|
||||
Upcoming Webinar
|
||||
</h5>
|
||||
<div class="text-body-1">
|
||||
Next Generation Frontend Architecture Using Layout Engine And Vue.
|
||||
</div>
|
||||
<div class="d-flex justify-space-between my-6 gap-4 flex-wrap">
|
||||
<div
|
||||
v-for="{ icon, title, value } in [{ icon: 'ri-calendar-line', title: '17 Nov 23', value: 'Date' }, { icon: 'ri-time-line', title: '32 Minutes', value: 'Duration' }]"
|
||||
:key="title"
|
||||
class="d-flex gap-x-4 align-center"
|
||||
>
|
||||
<VAvatar
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded
|
||||
>
|
||||
<VIcon :icon="icon" />
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VBtn block>
|
||||
Join the event
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
327
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal file
327
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useCalendarStore } from './useCalendarStore'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar7 from '@images/avatars/avatar-7.png'
|
||||
|
||||
// 👉 store
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
event: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'addEvent',
|
||||
'updateEvent',
|
||||
'removeEvent',
|
||||
])
|
||||
|
||||
const store = useCalendarStore()
|
||||
const refForm = ref()
|
||||
|
||||
// 👉 Event
|
||||
const event = ref(JSON.parse(JSON.stringify(props.event)))
|
||||
|
||||
const resetEvent = () => {
|
||||
event.value = JSON.parse(JSON.stringify(props.event))
|
||||
nextTick(() => {
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.isDrawerOpen, resetEvent)
|
||||
|
||||
const removeEvent = () => {
|
||||
emit('removeEvent', String(event.value.id))
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
refForm.value?.validate().then(({ valid }) => {
|
||||
if (valid) {
|
||||
|
||||
// If id exist on id => Update event
|
||||
if ('id' in event.value)
|
||||
emit('updateEvent', event.value)
|
||||
|
||||
// Else => add new event
|
||||
else
|
||||
emit('addEvent', event.value)
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const guestsOptions = [
|
||||
{
|
||||
avatar: avatar1,
|
||||
name: 'Jane Foster',
|
||||
},
|
||||
{
|
||||
avatar: avatar3,
|
||||
name: 'Donna Frank',
|
||||
},
|
||||
{
|
||||
avatar: avatar5,
|
||||
name: 'Gabrielle Robertson',
|
||||
},
|
||||
{
|
||||
avatar: avatar7,
|
||||
name: 'Lori Spears',
|
||||
},
|
||||
{
|
||||
avatar: avatar6,
|
||||
name: 'Sandy Vega',
|
||||
},
|
||||
{
|
||||
avatar: avatar2,
|
||||
name: 'Cheryl May',
|
||||
},
|
||||
]
|
||||
|
||||
// 👉 Form
|
||||
const onCancel = () => {
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
nextTick(() => {
|
||||
refForm.value?.reset()
|
||||
resetEvent()
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
|
||||
const startDateTimePickerConfig = computed(() => {
|
||||
const config = {
|
||||
enableTime: !event.value.allDay,
|
||||
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
|
||||
}
|
||||
|
||||
if (event.value.end)
|
||||
config.maxDate = event.value.end
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
const endDateTimePickerConfig = computed(() => {
|
||||
const config = {
|
||||
enableTime: !event.value.allDay,
|
||||
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
|
||||
}
|
||||
|
||||
if (event.value.start)
|
||||
config.minDate = event.value.start
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
location="end"
|
||||
:model-value="props.isDrawerOpen"
|
||||
width="420"
|
||||
class="scrollable-content"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
:title="event.id ? 'Update Event' : 'Add Event'"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
>
|
||||
<template #beforeClose>
|
||||
<IconBtn
|
||||
v-show="event.id"
|
||||
@click="removeEvent"
|
||||
>
|
||||
<VIcon
|
||||
size="18"
|
||||
icon="ri-delete-bin-7-line"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</AppDrawerHeaderSection>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<!-- SECTION Form -->
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<!-- 👉 Title -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.title"
|
||||
label="Title"
|
||||
placeholder="Meeting with Jane"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Calendar -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="event.extendedProps.calendar"
|
||||
label="Label"
|
||||
placeholder="Select Event Label"
|
||||
:rules="[requiredValidator]"
|
||||
:items="store.availableCalendars"
|
||||
:item-title="item => item.label"
|
||||
:item-value="item => item.label"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<div
|
||||
v-show="event.extendedProps.calendar"
|
||||
class="align-center"
|
||||
:class="event.extendedProps.calendar ? 'd-flex' : ''"
|
||||
>
|
||||
<VIcon
|
||||
size="8"
|
||||
icon="ri-circle-fill"
|
||||
:color="item.raw.color"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ item.raw.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item="{ item, props: itemProps }">
|
||||
<VListItem v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="8"
|
||||
icon="ri-circle-fill"
|
||||
:color="item.raw.color"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VSelect>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Start date -->
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
:key="JSON.stringify(startDateTimePickerConfig)"
|
||||
v-model="event.start"
|
||||
:rules="[requiredValidator]"
|
||||
label="Start date"
|
||||
placeholder="Select Date"
|
||||
:config="startDateTimePickerConfig"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 End date -->
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
:key="JSON.stringify(endDateTimePickerConfig)"
|
||||
v-model="event.end"
|
||||
:rules="[requiredValidator]"
|
||||
label="End date"
|
||||
placeholder="Select End Date"
|
||||
:config="endDateTimePickerConfig"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 All day -->
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="event.allDay"
|
||||
label="All day"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Event URL -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.url"
|
||||
label="Event URL"
|
||||
placeholder="https://event.com/meeting"
|
||||
:rules="[urlValidator]"
|
||||
type="url"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Guests -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="event.extendedProps.guests"
|
||||
label="Guests"
|
||||
placeholder="Select guests"
|
||||
:items="guestsOptions"
|
||||
:item-title="item => item.name"
|
||||
:item-value="item => item.name"
|
||||
chips
|
||||
multiple
|
||||
eager
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Location -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.extendedProps.location"
|
||||
label="Location"
|
||||
placeholder="Meeting room"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Description -->
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="event.extendedProps.description"
|
||||
label="Description"
|
||||
placeholder="Meeting description"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Form buttons -->
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-3"
|
||||
>
|
||||
Submit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
@click="onCancel"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<!-- !SECTION -->
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
1
resources/js/views/apps/calendar/types.js
Normal file
1
resources/js/views/apps/calendar/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
295
resources/js/views/apps/calendar/useCalendar.js
Normal file
295
resources/js/views/apps/calendar/useCalendar.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import { useConfigStore } from '@core/stores/config'
|
||||
import { useCalendarStore } from '@/views/apps/calendar/useCalendarStore'
|
||||
|
||||
export const blankEvent = {
|
||||
title: '',
|
||||
start: '',
|
||||
end: '',
|
||||
allDay: false,
|
||||
url: '',
|
||||
extendedProps: {
|
||||
/*
|
||||
ℹ️ We have to use undefined here because if we have blank string as value then select placeholder will be active (moved to top).
|
||||
Hence, we need to set it to undefined or null
|
||||
*/
|
||||
calendar: undefined,
|
||||
guests: [],
|
||||
location: '',
|
||||
description: '',
|
||||
},
|
||||
}
|
||||
export const useCalendar = (event, isEventHandlerSidebarActive, isLeftSidebarOpen) => {
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 👉 Store
|
||||
const store = useCalendarStore()
|
||||
|
||||
// 👉 Calendar template ref
|
||||
const refCalendar = ref()
|
||||
|
||||
|
||||
// 👉 Calendar colors
|
||||
const calendarsColor = {
|
||||
Business: 'primary',
|
||||
Holiday: 'success',
|
||||
Personal: 'error',
|
||||
Family: 'warning',
|
||||
ETC: 'info',
|
||||
}
|
||||
|
||||
|
||||
// ℹ️ Extract event data from event API
|
||||
const extractEventDataFromEventApi = eventApi => {
|
||||
const { id, title, start, end, url, extendedProps: { calendar, guests, location, description }, allDay } = eventApi
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
url,
|
||||
extendedProps: {
|
||||
calendar,
|
||||
guests,
|
||||
location,
|
||||
description,
|
||||
},
|
||||
allDay,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.server)
|
||||
store.fetchEvents()
|
||||
|
||||
|
||||
// 👉 Fetch events
|
||||
const fetchEvents = (info, successCallback) => {
|
||||
// If there's no info => Don't make useless API call
|
||||
if (!info)
|
||||
return
|
||||
store.fetchEvents()
|
||||
.then(r => {
|
||||
successCallback(r.map(e => ({
|
||||
...e,
|
||||
|
||||
// Convert string representation of date to Date object
|
||||
start: new Date(e.start),
|
||||
end: new Date(e.end),
|
||||
})))
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error occurred while fetching calendar events', e)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Calendar API
|
||||
const calendarApi = ref(null)
|
||||
|
||||
|
||||
// 👉 Update event in calendar [UI]
|
||||
const updateEventInCalendar = (updatedEventData, propsToUpdate, extendedPropsToUpdate) => {
|
||||
const existingEvent = calendarApi.value?.getEventById(String(updatedEventData.id))
|
||||
if (!existingEvent) {
|
||||
console.warn('Can\'t found event in calendar to update')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ---Set event properties except date related
|
||||
// Docs: https://fullcalendar.io/docs/Event-setProp
|
||||
// dateRelatedProps => ['start', 'end', 'allDay']
|
||||
for (let index = 0; index < propsToUpdate.length; index++) {
|
||||
const propName = propsToUpdate[index]
|
||||
|
||||
existingEvent.setProp(propName, updatedEventData[propName])
|
||||
}
|
||||
|
||||
// --- Set date related props
|
||||
// ? Docs: https://fullcalendar.io/docs/Event-setDates
|
||||
existingEvent.setDates(updatedEventData.start, updatedEventData.end, { allDay: updatedEventData.allDay })
|
||||
|
||||
// --- Set event's extendedProps
|
||||
// ? Docs: https://fullcalendar.io/docs/Event-setExtendedProp
|
||||
for (let index = 0; index < extendedPropsToUpdate.length; index++) {
|
||||
const propName = extendedPropsToUpdate[index]
|
||||
|
||||
existingEvent.setExtendedProp(propName, updatedEventData.extendedProps[propName])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 👉 Remove event in calendar [UI]
|
||||
const removeEventInCalendar = eventId => {
|
||||
const _event = calendarApi.value?.getEventById(eventId)
|
||||
if (_event)
|
||||
_event.remove()
|
||||
}
|
||||
|
||||
|
||||
// 👉 refetch events
|
||||
const refetchEvents = () => {
|
||||
calendarApi.value?.refetchEvents()
|
||||
}
|
||||
|
||||
watch(() => store.selectedCalendars, refetchEvents)
|
||||
|
||||
|
||||
// 👉 Add event
|
||||
const addEvent = _event => {
|
||||
store.addEvent(_event)
|
||||
.then(() => {
|
||||
refetchEvents()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Update event
|
||||
const updateEvent = _event => {
|
||||
store.updateEvent(_event)
|
||||
.then(r => {
|
||||
const propsToUpdate = ['id', 'title', 'url']
|
||||
const extendedPropsToUpdate = ['calendar', 'guests', 'location', 'description']
|
||||
|
||||
updateEventInCalendar(r, propsToUpdate, extendedPropsToUpdate)
|
||||
})
|
||||
refetchEvents()
|
||||
}
|
||||
|
||||
|
||||
// 👉 Remove event
|
||||
const removeEvent = eventId => {
|
||||
store.removeEvent(eventId).then(() => {
|
||||
removeEventInCalendar(eventId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Calendar options
|
||||
const calendarOptions = {
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
start: 'drawerToggler,prev,next title',
|
||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth',
|
||||
},
|
||||
events: fetchEvents,
|
||||
|
||||
// ❗ We need this to be true because when its false and event is allDay event and end date is same as start data then Full calendar will set end to null
|
||||
forceEventDuration: true,
|
||||
|
||||
/*
|
||||
Enable dragging and resizing event
|
||||
Docs: https://fullcalendar.io/docs/editable
|
||||
*/
|
||||
editable: true,
|
||||
|
||||
/*
|
||||
Enable resizing event from start
|
||||
Docs: https://fullcalendar.io/docs/eventResizableFromStart
|
||||
*/
|
||||
eventResizableFromStart: true,
|
||||
|
||||
/*
|
||||
Automatically scroll the scroll-containers during event drag-and-drop and date selecting
|
||||
Docs: https://fullcalendar.io/docs/dragScroll
|
||||
*/
|
||||
dragScroll: true,
|
||||
|
||||
/*
|
||||
Max number of events within a given day
|
||||
Docs: https://fullcalendar.io/docs/dayMaxEvents
|
||||
*/
|
||||
dayMaxEvents: 2,
|
||||
|
||||
/*
|
||||
Determines if day names and week names are clickable
|
||||
Docs: https://fullcalendar.io/docs/navLinks
|
||||
*/
|
||||
navLinks: true,
|
||||
eventClassNames({ event: calendarEvent }) {
|
||||
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
|
||||
|
||||
return [
|
||||
// Background Color
|
||||
`bg-light-${colorName} text-${colorName}`,
|
||||
]
|
||||
},
|
||||
eventClick({ event: clickedEvent, jsEvent }) {
|
||||
// Prevent the default action
|
||||
jsEvent.preventDefault()
|
||||
if (clickedEvent.url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(clickedEvent.url, '_blank')
|
||||
}
|
||||
|
||||
// * Only grab required field otherwise it goes in infinity loop
|
||||
// ! Always grab all fields rendered by form (even if it get `undefined`) otherwise due to Vue3/Composition API you might get: "object is not extensible"
|
||||
event.value = extractEventDataFromEventApi(clickedEvent)
|
||||
isEventHandlerSidebarActive.value = true
|
||||
},
|
||||
|
||||
// customButtons
|
||||
dateClick(info) {
|
||||
event.value = { ...event.value, start: info.date }
|
||||
isEventHandlerSidebarActive.value = true
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event drop (Also include dragged event)
|
||||
Docs: https://fullcalendar.io/docs/eventDrop
|
||||
We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
|
||||
*/
|
||||
eventDrop({ event: droppedEvent }) {
|
||||
updateEvent(extractEventDataFromEventApi(droppedEvent))
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event resize
|
||||
Docs: https://fullcalendar.io/docs/eventResize
|
||||
*/
|
||||
eventResize({ event: resizedEvent }) {
|
||||
if (resizedEvent.start && resizedEvent.end)
|
||||
updateEvent(extractEventDataFromEventApi(resizedEvent))
|
||||
},
|
||||
customButtons: {
|
||||
drawerToggler: {
|
||||
text: 'calendarDrawerToggler',
|
||||
click() {
|
||||
isLeftSidebarOpen.value = true
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// 👉 onMounted
|
||||
onMounted(() => {
|
||||
calendarApi.value = refCalendar.value.getApi()
|
||||
})
|
||||
|
||||
|
||||
// 👉 Jump to date on sidebar(inline) calendar change
|
||||
const jumpToDate = currentDate => {
|
||||
calendarApi.value?.gotoDate(new Date(currentDate))
|
||||
}
|
||||
|
||||
watch(() => configStore.isAppRTL, val => {
|
||||
calendarApi.value?.setOption('direction', val ? 'rtl' : 'ltr')
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
refCalendar,
|
||||
calendarOptions,
|
||||
refetchEvents,
|
||||
fetchEvents,
|
||||
addEvent,
|
||||
updateEvent,
|
||||
removeEvent,
|
||||
jumpToDate,
|
||||
}
|
||||
}
|
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,191 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useChat } from './useChat'
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const store = useChatStore()
|
||||
const { resolveAvatarBadgeVariant } = useChat()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="store.activeChat">
|
||||
<!-- Close Button -->
|
||||
<div
|
||||
class="pt-2 me-2"
|
||||
:class="$vuetify.locale.isRtl ? 'text-left' : 'text-right'"
|
||||
>
|
||||
<IconBtn @click="$emit('close')">
|
||||
<VIcon
|
||||
icon="ri-close-line"
|
||||
class="text-medium-emphasis"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<!-- User Avatar + Name + Role -->
|
||||
<div class="text-center px-6">
|
||||
<VBadge
|
||||
location="bottom right"
|
||||
offset-x="7"
|
||||
offset-y="4"
|
||||
bordered
|
||||
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
|
||||
class="chat-user-profile-badge mb-4"
|
||||
>
|
||||
<VAvatar
|
||||
size="84"
|
||||
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
|
||||
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="store.activeChat.contact.avatar"
|
||||
:src="store.activeChat.contact.avatar"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-3xl"
|
||||
>{{ avatarText(store.activeChat.contact.fullName) }}</span>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
<h5 class="text-h5">
|
||||
{{ store.activeChat.contact.fullName }}
|
||||
</h5>
|
||||
<p class="text-body-1 mb-0">
|
||||
{{ store.activeChat.contact.role }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- User Data -->
|
||||
<PerfectScrollbar
|
||||
class="ps-chat-user-profile-sidebar-content text-medium-emphasis pb-5 px-5"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<!-- About -->
|
||||
<div class="my-6">
|
||||
<p
|
||||
for="textarea-user-about"
|
||||
class="text-base text-disabled mb-1"
|
||||
>
|
||||
ABOUT
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ store.activeChat.contact.about }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Personal Information -->
|
||||
<div class="mb-6">
|
||||
<p class="text-base text-disabled mb-1">
|
||||
PERSONAL INFORMATION
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-mail-line"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
lucifer@email.com
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-phone-line"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
+1(123) 456 - 7890
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
icon="ri-time-line"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Mon - Fri 10AM - 8PM
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div>
|
||||
<p class="text-base text-disabled mb-1">
|
||||
OPTIONS
|
||||
</p>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-bookmark-line"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Add Tag
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-star-line"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Important Contact
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-file-image-line"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Shared Media
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
icon="ri-delete-bin-line"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
class="me-2"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Delete Contact
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
icon="ri-forbid-line"
|
||||
class="me-2"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Block Contact
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
color="error"
|
||||
append-icon="ri-delete-bin-7-line"
|
||||
class="mt-12"
|
||||
>
|
||||
Delete Contact
|
||||
</VBtn>
|
||||
</PerfectScrollbar>
|
||||
</template>
|
||||
</template>
|
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 }"
|
||||
:data-x="store.chatsContacts.length"
|
||||
>
|
||||
<VBadge
|
||||
dot
|
||||
location="bottom right"
|
||||
offset-x="3"
|
||||
offset-y="3"
|
||||
:color="resolveAvatarBadgeVariant(props.user.status)"
|
||||
bordered
|
||||
:model-value="props.isChatContact"
|
||||
>
|
||||
<VAvatar
|
||||
size="40"
|
||||
:variant="!props.user.avatar ? 'tonal' : undefined"
|
||||
:color="!props.user.avatar ? resolveAvatarBadgeVariant(props.user.status) : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="props.user.avatar"
|
||||
:src="props.user.avatar"
|
||||
alt="John Doe"
|
||||
/>
|
||||
<span v-else>{{ avatarText(user.fullName) }}</span>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
<div class="flex-grow-1 ms-4 overflow-hidden">
|
||||
<p class="text-base mb-0">
|
||||
{{ props.user.fullName }}
|
||||
</p>
|
||||
<span class="d-block text-body-2 text-truncate">{{ props.isChatContact && 'chat' in props.user ? props.user.chat.lastMessage.message : props.user.about }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.isChatContact && 'chat' in props.user"
|
||||
class="d-flex flex-column align-self-start"
|
||||
>
|
||||
<span class="d-block text-sm text-disabled whitespace-no-wrap">{{ formatDateToMonthShort(props.user.chat.lastMessage.time) }}</span>
|
||||
<VBadge
|
||||
v-if="props.user.chat.unseenMsgs"
|
||||
color="error"
|
||||
inline
|
||||
:content="props.user.chat.unseenMsgs"
|
||||
class="ms-auto"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/vuetify.scss";
|
||||
@use "@core-scss/base/mixins";
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
|
||||
.chat-contact {
|
||||
border-radius: vuetify.$border-radius-root;
|
||||
padding-block: 8px;
|
||||
padding-inline: var(--chat-content-spacing-x);
|
||||
|
||||
@include mixins.before-pseudo;
|
||||
@include vuetifyStates.states($active: false);
|
||||
|
||||
&.chat-contact-active {
|
||||
@include mixins.elevation(2);
|
||||
|
||||
background: rgb(var(--v-theme-primary));
|
||||
color: #fff;
|
||||
|
||||
--v-theme-on-background: #fff;
|
||||
|
||||
.v-avatar {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.v-badge--bordered .v-badge__badge::after {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
125
resources/js/views/apps/chat/ChatLeftSidebarContent.vue
Normal file
125
resources/js/views/apps/chat/ChatLeftSidebarContent.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useChat } from './useChat'
|
||||
import ChatContact from '@/views/apps/chat/ChatContact.vue'
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
const props = defineProps({
|
||||
search: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'openChatOfContact',
|
||||
'showUserProfile',
|
||||
'close',
|
||||
'update:search',
|
||||
])
|
||||
|
||||
const { resolveAvatarBadgeVariant } = useChat()
|
||||
const search = useVModel(props, 'search', emit)
|
||||
const store = useChatStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Chat list header -->
|
||||
<div
|
||||
v-if="store.profileUser"
|
||||
class="chat-list-header gap-4"
|
||||
>
|
||||
<VBadge
|
||||
dot
|
||||
location="bottom right"
|
||||
offset-x="3"
|
||||
offset-y="3"
|
||||
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
|
||||
bordered
|
||||
>
|
||||
<VAvatar
|
||||
class="cursor-pointer"
|
||||
@click="$emit('showUserProfile')"
|
||||
>
|
||||
<VImg
|
||||
:src="store.profileUser.avatar"
|
||||
alt="John Doe"
|
||||
/>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
|
||||
<VTextField
|
||||
v-model="search"
|
||||
placeholder="Search..."
|
||||
prepend-inner-icon="ri-search-line"
|
||||
density="compact"
|
||||
class="chat-list-search"
|
||||
/>
|
||||
|
||||
<IconBtn
|
||||
v-if="$vuetify.display.smAndDown"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-close-line"
|
||||
class="text-medium-emphasis"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar
|
||||
tag="ul"
|
||||
class="chat-contacts-list px-3 d-flex flex-column gap-1"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<li class="list-none">
|
||||
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Chats</span>
|
||||
</li>
|
||||
<ChatContact
|
||||
v-for="contact in store.chatsContacts"
|
||||
:key="`chat-${contact.id}`"
|
||||
:user="contact"
|
||||
is-chat-contact
|
||||
@click="$emit('openChatOfContact', contact.id)"
|
||||
/>
|
||||
<span
|
||||
v-show="!store.chatsContacts.length"
|
||||
class="no-chat-items-text text-disabled"
|
||||
>No chats found</span>
|
||||
<li class="list-none">
|
||||
<span class="chat-contact-header d-block text-primary text-lg font-weight-medium">Contacts</span>
|
||||
</li>
|
||||
<ChatContact
|
||||
v-for="contact in store.contacts"
|
||||
:key="`chat-${contact.id}`"
|
||||
:user="contact"
|
||||
@click="$emit('openChatOfContact', contact.id)"
|
||||
/>
|
||||
<span
|
||||
v-show="!store.contacts.length"
|
||||
class="no-chat-items-text text-disabled"
|
||||
>No contacts found</span>
|
||||
</PerfectScrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.chat-contacts-list {
|
||||
--chat-content-spacing-x: 12px;
|
||||
|
||||
padding-block-end: 0.75rem;
|
||||
|
||||
.chat-contact-header {
|
||||
margin-block: 1rem 4px;
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
.no-chat-items-text {
|
||||
margin-inline: var(--chat-content-spacing-x);
|
||||
}
|
||||
}
|
||||
</style>
|
149
resources/js/views/apps/chat/ChatLog.vue
Normal file
149
resources/js/views/apps/chat/ChatLog.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
const store = useChatStore()
|
||||
|
||||
const contact = computed(() => ({
|
||||
id: store.activeChat?.contact.id,
|
||||
avatar: store.activeChat?.contact.avatar,
|
||||
}))
|
||||
|
||||
const resolveFeedbackIcon = feedback => {
|
||||
if (feedback.isSeen)
|
||||
return {
|
||||
icon: 'ri-check-double-line',
|
||||
color: 'success',
|
||||
}
|
||||
else if (feedback.isDelivered)
|
||||
return {
|
||||
icon: 'ri-check-double-line',
|
||||
color: undefined,
|
||||
}
|
||||
else
|
||||
return {
|
||||
icon: 'ri-check-line',
|
||||
color: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const msgGroups = computed(() => {
|
||||
let messages = []
|
||||
const _msgGroups = []
|
||||
if (store.activeChat.chat) {
|
||||
messages = store.activeChat.chat.messages
|
||||
let msgSenderId = messages[0].senderId
|
||||
let msgGroup = {
|
||||
senderId: msgSenderId,
|
||||
messages: [],
|
||||
}
|
||||
messages.forEach((msg, index) => {
|
||||
if (msgSenderId === msg.senderId) {
|
||||
msgGroup.messages.push({
|
||||
message: msg.message,
|
||||
time: msg.time,
|
||||
feedback: msg.feedback,
|
||||
})
|
||||
} else {
|
||||
msgSenderId = msg.senderId
|
||||
_msgGroups.push(msgGroup)
|
||||
msgGroup = {
|
||||
senderId: msg.senderId,
|
||||
messages: [{
|
||||
message: msg.message,
|
||||
time: msg.time,
|
||||
feedback: msg.feedback,
|
||||
}],
|
||||
}
|
||||
}
|
||||
if (index === messages.length - 1)
|
||||
_msgGroups.push(msgGroup)
|
||||
})
|
||||
}
|
||||
|
||||
return _msgGroups
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-log pa-5">
|
||||
<div
|
||||
v-for="(msgGrp, index) in msgGroups"
|
||||
:key="msgGrp.senderId + String(index)"
|
||||
class="chat-group d-flex align-start"
|
||||
:class="[{
|
||||
'flex-row-reverse': msgGrp.senderId !== contact.id,
|
||||
'mb-8': msgGroups.length - 1 !== index,
|
||||
}]"
|
||||
>
|
||||
<div
|
||||
class="chat-avatar"
|
||||
:class="msgGrp.senderId !== contact.id ? 'ms-4' : 'me-4'"
|
||||
>
|
||||
<VAvatar size="32">
|
||||
<VImg :src="msgGrp.senderId === contact.id ? contact.avatar : store.profileUser?.avatar" />
|
||||
</VAvatar>
|
||||
</div>
|
||||
<div
|
||||
class="chat-body d-inline-flex flex-column"
|
||||
:class="msgGrp.senderId !== contact.id ? 'align-end' : 'align-start'"
|
||||
>
|
||||
<div
|
||||
v-for="(msgData, msgIndex) in msgGrp.messages"
|
||||
:key="msgData.time"
|
||||
class="chat-content text-body-1 py-2 px-4 elevation-2"
|
||||
:class="[
|
||||
msgGrp.senderId === contact.id ? 'bg-surface chat-left' : 'bg-primary text-white chat-right',
|
||||
msgGrp.messages.length - 1 !== msgIndex ? 'mb-2' : 'mb-1',
|
||||
]"
|
||||
>
|
||||
<p class="mb-0">
|
||||
{{ msgData.message }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
:class="{ 'text-right': msgGrp.senderId !== contact.id }"
|
||||
class="d-flex align-center gap-2"
|
||||
>
|
||||
<VIcon
|
||||
v-if="msgGrp.senderId !== contact.id"
|
||||
size="16"
|
||||
:color="resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).color"
|
||||
>
|
||||
{{ resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).icon }}
|
||||
</VIcon>
|
||||
<p
|
||||
class="text-sm text-disabled mb-0"
|
||||
style="letter-spacing: 0.4px;"
|
||||
>
|
||||
{{ formatDate(msgGrp.messages[msgGrp.messages.length - 1].time, { hour: 'numeric', minute: 'numeric' }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang=scss>
|
||||
.chat-log {
|
||||
.chat-content {
|
||||
border-end-end-radius: 6px;
|
||||
border-end-start-radius: 6px;
|
||||
|
||||
p {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&.bg-surface{
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
&.chat-left {
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
|
||||
&.chat-right {
|
||||
border-start-start-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
196
resources/js/views/apps/chat/ChatUserProfileSidebarContent.vue
Normal file
196
resources/js/views/apps/chat/ChatUserProfileSidebarContent.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useChat } from './useChat'
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
|
||||
// composables
|
||||
const store = useChatStore()
|
||||
const { resolveAvatarBadgeVariant } = useChat()
|
||||
|
||||
const userStatusRadioOptions = [
|
||||
{
|
||||
title: 'Online',
|
||||
value: 'online',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Away',
|
||||
value: 'away',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Do not disturb',
|
||||
value: 'busy',
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Offline',
|
||||
value: 'offline',
|
||||
color: 'secondary',
|
||||
},
|
||||
]
|
||||
|
||||
const isTwoStepVerified = ref(true)
|
||||
const isNotificationEnabled = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="store.profileUser">
|
||||
<!-- Close Button -->
|
||||
<div class="pt-2 me-2 text-end">
|
||||
<IconBtn @click="$emit('close')">
|
||||
<VIcon
|
||||
class="text-medium-emphasis"
|
||||
icon="ri-close-line"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<!-- User Avatar + Name + Role -->
|
||||
<div class="text-center px-6">
|
||||
<VBadge
|
||||
location="bottom right"
|
||||
offset-x="7"
|
||||
offset-y="4"
|
||||
bordered
|
||||
:color="resolveAvatarBadgeVariant(store.profileUser.status)"
|
||||
class="chat-user-profile-badge mb-4"
|
||||
>
|
||||
<VAvatar
|
||||
size="84"
|
||||
:variant="!store.profileUser.avatar ? 'tonal' : undefined"
|
||||
:color="!store.profileUser.avatar ? resolveAvatarBadgeVariant(store.profileUser.status) : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="store.profileUser.avatar"
|
||||
:src="store.profileUser.avatar"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-3xl"
|
||||
>{{ avatarText(store.profileUser.fullName) }}</span>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
<h5 class="text-h5">
|
||||
{{ store.profileUser.fullName }}
|
||||
</h5>
|
||||
<p class="text-body-1 text-capitalize mb-0">
|
||||
{{ store.profileUser.role }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- User Data -->
|
||||
<PerfectScrollbar
|
||||
class="ps-chat-user-profile-sidebar-content pb-5 px-5"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<!-- About -->
|
||||
<div class="my-6 text-medium-emphasis">
|
||||
<p
|
||||
for="textarea-user-about"
|
||||
class="text-base text-disabled mb-0"
|
||||
>
|
||||
ABOUT
|
||||
</p>
|
||||
<VTextarea
|
||||
id="textarea-user-about"
|
||||
v-model="store.profileUser.about"
|
||||
auto-grow
|
||||
class="mt-1"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="mb-6">
|
||||
<p class="text-base text-disabled mb-0">
|
||||
STATUS
|
||||
</p>
|
||||
<VRadioGroup
|
||||
v-model="store.profileUser.status"
|
||||
class="ms-2 mt-1"
|
||||
>
|
||||
<VRadio
|
||||
v-for="radioOption in userStatusRadioOptions"
|
||||
:key="radioOption.title"
|
||||
:label="radioOption.title"
|
||||
:value="radioOption.value"
|
||||
:color="radioOption.color"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="text-medium-emphasis">
|
||||
<p class="text-base text-disabled mb-0">
|
||||
SETTINGS
|
||||
</p>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="ri-lock-password-line"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Two-step Verification
|
||||
</h6>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<VSwitch v-model="isTwoStepVerified" />
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="ri-notification-line"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Notification
|
||||
</h6>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<VSwitch v-model="isNotificationEnabled" />
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="ri-user-add-line"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Invite Friends
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex align-center pa-2">
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="ri-delete-bin-7-line"
|
||||
size="22"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Delete Account
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<VBtn
|
||||
block
|
||||
color="primary"
|
||||
class="mt-11"
|
||||
append-icon="ri-logout-box-r-line"
|
||||
>
|
||||
Logout
|
||||
</VBtn>
|
||||
</PerfectScrollbar>
|
||||
</template>
|
||||
</template>
|
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
|
||||
},
|
||||
},
|
||||
})
|
200
resources/js/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue
Normal file
200
resources/js/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen'])
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
|
||||
const refVForm = ref()
|
||||
const name = ref()
|
||||
const email = ref()
|
||||
const mobile = ref()
|
||||
const addressLine1 = ref()
|
||||
const addressLine2 = ref()
|
||||
const town = ref()
|
||||
const state = ref()
|
||||
const postCode = ref()
|
||||
const country = ref()
|
||||
const isBillingAddress = ref(false)
|
||||
|
||||
const resetForm = () => {
|
||||
refVForm.value?.reset()
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
:model-value="props.isDrawerOpen"
|
||||
temporary
|
||||
location="end"
|
||||
width="370"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Add a Customer"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
<VDivider />
|
||||
|
||||
<VCard flat>
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="h-100"
|
||||
>
|
||||
<VCardText style="block-size: calc(100vh - 5rem);">
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<div class="text-body-1 font-weight-medium text-high-emphasis">
|
||||
Basic Information
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="name"
|
||||
label="Full Name"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="email"
|
||||
label="Email Address"
|
||||
:rules="[requiredValidator, emailValidator]"
|
||||
placeholder="johndoe@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="mobile"
|
||||
label="Mobile Number"
|
||||
type="number"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="+(123) 456-7890"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol>
|
||||
<div class="text-body-1 font-weight-medium text-high-emphasis">
|
||||
Shipping Information
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="addressLine1"
|
||||
label="Address Line 1*"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="45, Rocker Terrace"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="addressLine2"
|
||||
placeholder="Empire Heights,"
|
||||
:rules="[requiredValidator]"
|
||||
label="Address Line 2*"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="town"
|
||||
label="Town*"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="New York"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="state"
|
||||
placeholder="Texas"
|
||||
:rules="[requiredValidator]"
|
||||
label="State/Province*"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="postCode"
|
||||
label="Post Code*"
|
||||
type="number"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="982347"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="country"
|
||||
placeholder="United States"
|
||||
:rules="[requiredValidator]"
|
||||
label="Country"
|
||||
:items="['United States', 'United Kingdom', 'Canada']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<h6 class="text-h6">
|
||||
Use as a billing address?
|
||||
</h6>
|
||||
<span class="text-sm">Please check budget for more info</span>
|
||||
</div>
|
||||
<VSwitch v-model="isBillingAddress" />
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="me-4"
|
||||
>
|
||||
Add
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="resetForm"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
</style>
|
282
resources/js/views/apps/ecommerce/EcommerceAddCategoryDrawer.vue
Normal file
282
resources/js/views/apps/ecommerce/EcommerceAddCategoryDrawer.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<script setup>
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import {
|
||||
EditorContent,
|
||||
useEditor,
|
||||
} from '@tiptap/vue-3'
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen'])
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
content: '',
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Image,
|
||||
Placeholder.configure({ placeholder: 'Write a Comment...' }),
|
||||
Underline,
|
||||
Link.configure({ openOnClick: false }),
|
||||
],
|
||||
})
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.value?.getAttributes('link').href
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null)
|
||||
return
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
const addImage = () => {
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL')
|
||||
if (url)
|
||||
editor.value?.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
|
||||
const refVForm = ref()
|
||||
const categoryTitle = ref()
|
||||
const categorySlug = ref()
|
||||
const categoryImg = ref()
|
||||
const parentCategory = ref()
|
||||
const parentStatus = ref()
|
||||
|
||||
const resetForm = () => {
|
||||
emit('update:isDrawerOpen', false)
|
||||
refVForm.value?.reset()
|
||||
editor.value?.commands.clearContent()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
:model-value="props.isDrawerOpen"
|
||||
temporary
|
||||
location="end"
|
||||
width="370"
|
||||
class="category-navigation-drawer scrollable-content"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Add Category"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="categoryTitle"
|
||||
label="Title"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="Fashion"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="categorySlug"
|
||||
label="Slug"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="Trends fashion"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VFileInput
|
||||
v-model="categoryImg"
|
||||
prepend-icon=""
|
||||
:rules="[requiredValidator]"
|
||||
density="compact"
|
||||
label="No file chosen"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<VBtn variant="outlined">
|
||||
Choose
|
||||
</VBtn>
|
||||
</template>
|
||||
</VFileInput>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="parentCategory"
|
||||
:rules="[requiredValidator]"
|
||||
label="Parent Category"
|
||||
placeholder="Select Parent Category"
|
||||
:items="['HouseHold', 'Management', 'Electronics', 'Office', 'Accessories']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="tiptap-editor-wrapper rounded py-2 px-4">
|
||||
<EditorContent :editor="editor" />
|
||||
<div
|
||||
v-if="editor"
|
||||
class="d-flex justify-end flex-wrap gap-x-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-bold"
|
||||
:color="editor.isActive('bold') ? 'primary' : ''"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('underline') ? 'primary' : ''"
|
||||
icon="ri-underline"
|
||||
size="20"
|
||||
@click="editor.commands.toggleUnderline()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('italic') ? 'primary' : ''"
|
||||
icon="ri-italic"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('bulletList') ? 'primary' : ''"
|
||||
icon="ri-list-check"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('orderedList') ? 'primary' : ''"
|
||||
icon="ri-list-ordered-2"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
icon="ri-links-line"
|
||||
size="20"
|
||||
@click="setLink"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
icon="ri-image-line"
|
||||
size="20"
|
||||
@click="addImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="parentStatus"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="Select Category Status"
|
||||
label="Parent Status"
|
||||
:items="['Published', 'Inactive', 'Scheduled']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="me-4"
|
||||
>
|
||||
Add
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="resetForm"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.category-navigation-drawer {
|
||||
.ProseMirror {
|
||||
padding: 0.5rem;
|
||||
block-size: auto;
|
||||
min-block-size: 6.25rem;
|
||||
|
||||
p {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
p.is-editor-empty:first-child::before {
|
||||
block-size: 0;
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: inline-start;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
border-color: rgba(var(--v-theme-primary), var(--v-border-opacity)) !important;
|
||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.ProseMirror-focused{
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.tiptap-editor-wrapper {
|
||||
border: 1px solid rgba(var(--v-border-color), 0.22);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-border-color), 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,197 @@
|
||||
<script setup>
|
||||
import rocketImg from '@images/eCommerce/rocket.png'
|
||||
|
||||
const props = defineProps({
|
||||
customerData: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isUserInfoEditDialogVisible = ref(false)
|
||||
const isUpgradePlanDialogVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- SECTION Customer Details -->
|
||||
<VCol cols="12">
|
||||
<VCard v-if="props.customerData">
|
||||
<VCardText class="text-center pt-15">
|
||||
<!-- 👉 Avatar -->
|
||||
<VAvatar
|
||||
rounded
|
||||
:size="120"
|
||||
:color="!props.customerData.customer ? 'primary' : undefined"
|
||||
:variant="!props.customerData.avatar ? 'tonal' : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="props.customerData.avatar"
|
||||
:src="props.customerData.avatar"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-5xl font-weight-medium"
|
||||
>
|
||||
User Not Availaable
|
||||
</span>
|
||||
</VAvatar>
|
||||
|
||||
<!-- 👉 Customer fullName -->
|
||||
<h6 class="text-h5 mt-4">
|
||||
{{ props.customerData.customer }}
|
||||
</h6>
|
||||
<p class="text-body-1 mb-0">
|
||||
Customer ID #{{ props.customerData.customerId }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex justify-space-evenly gap-x-12 mt-6">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
rounded
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon icon="ri-shopping-cart-line" />
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column align-start">
|
||||
<h5 class="text-h5">
|
||||
{{ props.customerData.order }}
|
||||
</h5>
|
||||
<span class="text-body-1">Orders</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
rounded
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon icon="ri-money-dollar-circle-line" />
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column align-start">
|
||||
<h5 class="text-h5">
|
||||
{{ Math.round(props.customerData.totalSpent) }}
|
||||
</h5>
|
||||
<span class="text-body-1">Spent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Customer Details -->
|
||||
<VCardText>
|
||||
<h5 class="text-h5">
|
||||
Details
|
||||
</h5>
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VList class="card-list mt-2">
|
||||
<VListItem>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-medium me-2">Username:</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.customerData.customer }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-medium me-2">Billing Email:</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.customerData.email }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-medium me-2">Status:</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.customerData.status }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-medium me-2">Contact:</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.customerData.contact }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-medium me-2">Country:</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.customerData.country }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<VBtn
|
||||
block
|
||||
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
|
||||
>
|
||||
Edit Details
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION Upgrade to Premium -->
|
||||
<VCol cols="12">
|
||||
<VCard
|
||||
flat
|
||||
class="current-plan"
|
||||
color="primary"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center">
|
||||
<div>
|
||||
<h5 class="text-h5 text-white mb-4">
|
||||
Upgrade to premium
|
||||
</h5>
|
||||
<p class="mb-6 text-wrap">
|
||||
Upgrade customer to premium membership to access pro features.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<VImg
|
||||
:src="rocketImg"
|
||||
height="108"
|
||||
width="108"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
color="#fff"
|
||||
class="text-primary"
|
||||
block
|
||||
@click="isUpgradePlanDialogVisible = !isUpgradePlanDialogVisible"
|
||||
>
|
||||
Upgrade to Premium
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<!-- !SECTION -->
|
||||
</VRow>
|
||||
<UserInfoEditDialog v-model:isDialogVisible="isUserInfoEditDialogVisible" />
|
||||
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,194 @@
|
||||
<script setup>
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Order',
|
||||
key: 'order',
|
||||
},
|
||||
{
|
||||
title: 'Date',
|
||||
key: 'date',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Spent',
|
||||
key: 'spent',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const resolveStatus = status => {
|
||||
if (status === 'Delivered')
|
||||
return { color: 'success' }
|
||||
if (status === 'Out for Delivery')
|
||||
return { color: 'primary' }
|
||||
if (status === 'Ready to Pickup')
|
||||
return { color: 'info' }
|
||||
if (status === 'Dispatched')
|
||||
return { color: 'warning' }
|
||||
}
|
||||
|
||||
const {
|
||||
data: ordersData,
|
||||
execute: fetchOrders,
|
||||
} = await useApi(createUrl('/apps/ecommerce/orders', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const orders = computed(() => ordersData.value?.orders || [])
|
||||
const totalOrder = computed(() => ordersData.value?.total || 0)
|
||||
|
||||
const deleteOrder = async id => {
|
||||
await $api(`/apps/ecommerce/orders/${ id }`, { method: 'DELETE' })
|
||||
fetchOrders()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-sm-space-between justify-start flex-wrap gap-4">
|
||||
<div class="text-h5">
|
||||
Orders placed
|
||||
</div>
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Order"
|
||||
density="compact"
|
||||
style=" max-inline-size: 250px; min-inline-size: 200px;"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="orders"
|
||||
item-value="id"
|
||||
:items-length="totalOrder"
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- Order ID -->
|
||||
<template #item.order="{ item }">
|
||||
<RouterLink :to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }">
|
||||
#{{ item.order }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- Date -->
|
||||
<template #item.date="{ item }">
|
||||
{{ new Date(item.date).toDateString() }}
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
size="small"
|
||||
:color="resolveStatus(item.status)?.color"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Spent -->
|
||||
<template #item.spent="{ item }">
|
||||
${{ item.spent }}
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-fill" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem value="view">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
View
|
||||
</RouterLink>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
value="delete"
|
||||
@click="deleteOrder(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalOrder) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalOrder / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalOrder / itemsPerPage) ? page = Math.ceil(totalOrder / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,427 @@
|
||||
<script setup>
|
||||
import usFlag from '@images/icons/countries/us.png'
|
||||
import americanExpress from '@images/icons/payments/img/american-express.png'
|
||||
import mastercard from '@images/icons/payments/img/mastercard.png'
|
||||
import visa from '@images/icons/payments/img/visa-light.png'
|
||||
|
||||
const show = ref([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
])
|
||||
|
||||
const paymentShow = ref([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
])
|
||||
|
||||
const isEditAddressDialogVisible = ref(false)
|
||||
const isCardAddDialogVisible = ref(false)
|
||||
const isNewEditAddressDialogVisible = ref(false)
|
||||
const isNewCardAddDialogVisible = ref(false)
|
||||
|
||||
const currentCardDetails = {
|
||||
number: '1234 5678 9012 3456',
|
||||
name: 'John Doe',
|
||||
expiry: '12/2028',
|
||||
cvv: '123',
|
||||
isPrimary: false,
|
||||
type: '',
|
||||
}
|
||||
|
||||
const editBillingData = {
|
||||
firstName: 'Gertrude',
|
||||
lastName: 'Jennings',
|
||||
selectedCountry: 'USA',
|
||||
addressLine1: '100 Water Plant Avenue',
|
||||
addressLine2: 'Building 1303 Wake Island',
|
||||
landmark: 'Near Wake Island',
|
||||
contact: '+1(609) 933-44-22',
|
||||
country: 'USA',
|
||||
state: 'Queensland',
|
||||
zipCode: 403114,
|
||||
}
|
||||
|
||||
const addressData = [
|
||||
{
|
||||
title: 'Home',
|
||||
subtitle: '23 Shatinon Mekalan',
|
||||
owner: 'Violet Mendoza',
|
||||
defaultAddress: true,
|
||||
address: ` 23 Shatinon Mekalan,
|
||||
<br>
|
||||
Melbourne, VIC 3000,
|
||||
<br>
|
||||
LondonUK`,
|
||||
},
|
||||
{
|
||||
title: 'Office',
|
||||
subtitle: '45 Rocker Terrace',
|
||||
owner: 'Violet Mendoza',
|
||||
defaultAddress: false,
|
||||
address: ` 45 Rocker Terrace,
|
||||
<br>
|
||||
Latheronwheel,
|
||||
<br>
|
||||
KW5 8NW, London,
|
||||
<br>
|
||||
UK`,
|
||||
},
|
||||
{
|
||||
title: 'Family',
|
||||
subtitle: '512 Water Plant',
|
||||
owner: 'Violet Mendoza',
|
||||
defaultAddress: false,
|
||||
address: ` 512 Water Plant,
|
||||
<br>
|
||||
Melbourne, VIC 3000,
|
||||
<br>
|
||||
LondonUK`,
|
||||
},
|
||||
]
|
||||
|
||||
const paymentData = [
|
||||
{
|
||||
title: 'Mastercard',
|
||||
subtitle: 'Expries Apr 2028',
|
||||
isDefaultMethod: false,
|
||||
image: mastercard,
|
||||
},
|
||||
{
|
||||
title: 'American Express',
|
||||
subtitle: 'Expries Apr 2028',
|
||||
isDefaultMethod: false,
|
||||
image: americanExpress,
|
||||
},
|
||||
{
|
||||
title: 'Visa',
|
||||
subtitle: '45 Roker Terrace',
|
||||
isDefaultMethod: true,
|
||||
image: visa,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- 👉 Address Book -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between mb-5 flex-wrap align-center gap-y-4 gap-x-6">
|
||||
<h5 class="text-h5">
|
||||
Address Book
|
||||
</h5>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@click="isNewEditAddressDialogVisible = !isNewEditAddressDialogVisible"
|
||||
>
|
||||
Add new Address
|
||||
</VBtn>
|
||||
</div>
|
||||
<template
|
||||
v-for="(address, index) in addressData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="d-flex justify-space-between mb-3 gap-y-2 flex-wrap align-center">
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<IconBtn
|
||||
density="comfortable"
|
||||
@click="show[index] = !show[index]"
|
||||
>
|
||||
<VIcon
|
||||
:icon="show[index] ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'"
|
||||
class="flip-in-rtl text-high-emphasis"
|
||||
/>
|
||||
</IconBtn>
|
||||
|
||||
<div>
|
||||
<div class="d-flex align-center mb-1">
|
||||
<h6 class="text-h6 me-2">
|
||||
{{ address.title }}
|
||||
</h6>
|
||||
<VChip
|
||||
v-if="address.defaultAddress"
|
||||
color="success"
|
||||
size="small"
|
||||
>
|
||||
Default Address
|
||||
</VChip>
|
||||
</div>
|
||||
<span class="text-body-1">{{ address.subtitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-11">
|
||||
<IconBtn @click="isEditAddressDialogVisible = true">
|
||||
<VIcon
|
||||
icon="ri-edit-box-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="ri-delete-bin-7-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="ri-more-2-fill"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
<VExpandTransition>
|
||||
<div
|
||||
v-show="show[index]"
|
||||
class="ps-12"
|
||||
>
|
||||
<div class="mb-1 font-weight-medium text-high-emphasis">
|
||||
{{ address.owner }}
|
||||
</div>
|
||||
<div v-html="address.address" />
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
<VDivider
|
||||
v-if="index !== addressData.length - 1"
|
||||
class="my-3"
|
||||
/>
|
||||
</template>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Payment Methods -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between mb-5 flex-wrap align-center gap-y-4 gap-x-6">
|
||||
<h5 class="text-h5">
|
||||
Payment Methods
|
||||
</h5>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@click="isNewCardAddDialogVisible = !isNewCardAddDialogVisible"
|
||||
>
|
||||
Add Payment Methods
|
||||
</VBtn>
|
||||
</div>
|
||||
<template
|
||||
v-for="(payment, index) in paymentData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="d-flex justify-space-between mb-4 gap-y-2 flex-wrap align-center">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<IconBtn
|
||||
density="comfortable"
|
||||
@click="paymentShow[index] = !paymentShow[index]"
|
||||
>
|
||||
<VIcon
|
||||
:icon="paymentShow[index] ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'"
|
||||
class="flip-in-rtl text-high-emphasis"
|
||||
/>
|
||||
</IconBtn>
|
||||
|
||||
<VImg
|
||||
:src="payment.image"
|
||||
height="30"
|
||||
width="50"
|
||||
class="me-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="d-flex flex-wrap mb-1">
|
||||
<h6 class="text-h6 me-2">
|
||||
{{ payment.title }}
|
||||
</h6>
|
||||
<VChip
|
||||
v-if="payment.isDefaultMethod"
|
||||
color="success"
|
||||
density="comfortable"
|
||||
>
|
||||
Default Method
|
||||
</VChip>
|
||||
</div>
|
||||
<span class="text-body-1">{{ payment.subtitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-11">
|
||||
<IconBtn @click="isCardAddDialogVisible = true">
|
||||
<VIcon
|
||||
icon="ri-edit-box-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="ri-delete-bin-7-line"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="ri-more-2-fill"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
<VExpandTransition>
|
||||
<div
|
||||
v-show="paymentShow[index]"
|
||||
class="ps-12"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTable>
|
||||
<tr>
|
||||
<td
|
||||
class="text-sm pb-1"
|
||||
style="inline-size: 100px;"
|
||||
>
|
||||
Name
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
Violet Mendoza
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Number
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
**** 4487
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Expires
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
08/2028
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Type
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
Master Card
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Issuer
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
VICBANK
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
ID
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
DH73DJ8
|
||||
</td>
|
||||
</tr>
|
||||
</VTable>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTable>
|
||||
<tr>
|
||||
<td
|
||||
class="text-sm pb-1"
|
||||
style="inline-size: 100px;"
|
||||
>
|
||||
Billing
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
United Kingdom
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Number
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
+7634 983 637
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Email
|
||||
</td>
|
||||
<td class="text-sm text-high-emphasis font-weight-medium">
|
||||
vafgot@vultukir.org
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
Origin
|
||||
</td>
|
||||
<td class="d-flex">
|
||||
<div class="text-body-2 font-weight-medium text-high-emphasis me-2">
|
||||
United States
|
||||
</div>
|
||||
<img
|
||||
:src="usFlag"
|
||||
height="20"
|
||||
width="20"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm pb-1">
|
||||
CVC
|
||||
</td>
|
||||
<td class="d-flex">
|
||||
<div class="text-body-2 font-weight-medium text-high-emphasis me-2">
|
||||
Passed
|
||||
</div>
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="success"
|
||||
size="20"
|
||||
inline
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-check-line"
|
||||
color="success"
|
||||
size="12"
|
||||
/>
|
||||
</VAvatar>
|
||||
</td>
|
||||
</tr>
|
||||
</VTable>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
<VDivider
|
||||
v-if="index !== paymentData.length - 1"
|
||||
class="my-4"
|
||||
/>
|
||||
</template>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<AddEditAddressDialog
|
||||
v-model:isDialogVisible="isEditAddressDialogVisible"
|
||||
:billing-address="editBillingData"
|
||||
/>
|
||||
<AddEditAddressDialog v-model:isDialogVisible="isNewEditAddressDialogVisible" />
|
||||
<CardAddEditDialog
|
||||
v-model:isDialogVisible="isCardAddDialogVisible"
|
||||
:card-details="currentCardDetails"
|
||||
/>
|
||||
<CardAddEditDialog v-model:isDialogVisible="isNewCardAddDialogVisible" />
|
||||
</template>
|
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
const notifications = ref([
|
||||
{
|
||||
type: 'New for you',
|
||||
email: true,
|
||||
browser: false,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Account activity',
|
||||
email: false,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'A new browser used to sign in',
|
||||
email: true,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'A new device is linked',
|
||||
email: false,
|
||||
browser: true,
|
||||
app: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="Notifications">
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
You will receive notification for the below selected items.
|
||||
</h6>
|
||||
</VCardText>
|
||||
<VTable class="text-no-wrap rounded-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
TYPE
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
</th>
|
||||
<th scope="col">
|
||||
BROWSER
|
||||
</th>
|
||||
<th scope="col">
|
||||
APP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="notification in notifications"
|
||||
:key="notification.type"
|
||||
>
|
||||
<td class="text-high-emphasis">
|
||||
{{ notification.type }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.email" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.browser" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.app" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<VBtn>Save changes</VBtn>
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
import CustomerOrderTable from './CustomerOrderTable.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex gap-y-2 flex-column">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
icon="ri-money-dollar-circle-line"
|
||||
rounded
|
||||
/>
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
Account Balance
|
||||
</h6>
|
||||
<div class="text-base">
|
||||
<p class="mb-0">
|
||||
<span class="text-primary font-weight-medium text-lg me-1">$7480</span>
|
||||
Credit Left
|
||||
</p>
|
||||
<p class="mb-0 text-base">
|
||||
Account balance for next purchase
|
||||
</p>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex gap-y-2 flex-column">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="success"
|
||||
icon="ri-gift-line"
|
||||
rounded
|
||||
/>
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
Loyalty Program
|
||||
</h6>
|
||||
<div>
|
||||
<VChip
|
||||
color="success"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
>
|
||||
Platinum Member
|
||||
</VChip>
|
||||
|
||||
<p class="mb-0 text-base">
|
||||
3000 points to next tier
|
||||
</p>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex gap-y-2 flex-column">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
icon="ri-star-smile-line"
|
||||
rounded
|
||||
/>
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
Wishlist
|
||||
</h6>
|
||||
<div>
|
||||
<p class=" mb-0">
|
||||
<span class="text-warning font-weight-medium text-lg me-1">15</span>
|
||||
items in wishlist
|
||||
</p>
|
||||
<p class="mb-0 text-base">
|
||||
Receive notification when items go on sale
|
||||
</p>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex gap-y-2 flex-column">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="info"
|
||||
icon="ri-vip-crown-line"
|
||||
rounded
|
||||
/>
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
Coupons
|
||||
</h6>
|
||||
<div>
|
||||
<p class="mb-0">
|
||||
<span class="text-info text-lg me-2">21</span>
|
||||
Coupons you win
|
||||
</p>
|
||||
<p class="mb-0 text-base">
|
||||
Use coupon on next purchase
|
||||
</p>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol>
|
||||
<CustomerOrderTable />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
@@ -0,0 +1,201 @@
|
||||
<script setup>
|
||||
import chrome from '@images/logos/chrome.png'
|
||||
|
||||
const isNewPasswordVisible = ref(false)
|
||||
const isConfirmPasswordVisible = ref(false)
|
||||
const smsVerificationNumber = ref('+1(968) 819-2547')
|
||||
const isTwoFactorDialogOpen = ref(false)
|
||||
|
||||
const recentDeviceHeader = [
|
||||
{
|
||||
title: 'BROWSER',
|
||||
key: 'browser',
|
||||
},
|
||||
{
|
||||
title: 'DEVICE',
|
||||
key: 'device',
|
||||
},
|
||||
{
|
||||
title: 'LOCATION',
|
||||
key: 'location',
|
||||
},
|
||||
{
|
||||
title: 'RECENT ACTIVITY',
|
||||
key: 'activity',
|
||||
},
|
||||
]
|
||||
|
||||
const recentDevices = [
|
||||
{
|
||||
browser: 'Chrome on Windows',
|
||||
logo: chrome,
|
||||
device: 'Dell XPS 15',
|
||||
location: 'United States',
|
||||
activity: '10, Jan 2020 20:07',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on Android',
|
||||
logo: chrome,
|
||||
device: 'Google Pixel 3a',
|
||||
location: 'Ghana',
|
||||
activity: '11, Jan 2020 10:16',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on macOS',
|
||||
logo: chrome,
|
||||
device: 'Apple iMac',
|
||||
location: 'Mayotte',
|
||||
activity: '11, Jan 2020 12:10',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on iPhone',
|
||||
logo: chrome,
|
||||
device: 'Apple iPhone XR',
|
||||
location: 'Mauritania',
|
||||
activity: '12, Jan 2020 8:29',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Change password -->
|
||||
<VCard title="Change Password">
|
||||
<VCardText>
|
||||
<VAlert
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
closable
|
||||
class="mb-4"
|
||||
>
|
||||
<VAlertTitle>Ensure that these requirements are met</VAlertTitle>
|
||||
<span>Minimum 8 characters long, uppercase & symbol</span>
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="New Password"
|
||||
placeholder="············"
|
||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Confirm Password"
|
||||
placeholder="············"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VBtn type="submit">
|
||||
Change Password
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Two step verification -->
|
||||
<VCard
|
||||
title="Two-step verification"
|
||||
subtitle="Keep your account secure with authentication step."
|
||||
>
|
||||
<VCardText>
|
||||
<div>
|
||||
<h4 class="font-weight-medium mb-1">
|
||||
SMS
|
||||
</h4>
|
||||
<VTextField
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:model-value="smsVerificationNumber"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<VBtn
|
||||
icon
|
||||
rounded
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
class="me-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-edit-box-line"
|
||||
size="24"
|
||||
@click="isTwoFactorDialogOpen = true"
|
||||
/>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
icon
|
||||
rounded
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
icon="ri-user-add-line"
|
||||
/>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
|
||||
<p class="mb-0 mt-4">
|
||||
Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a
|
||||
href="javascript:void(0)"
|
||||
class="text-decoration-none"
|
||||
>Learn more</a>.
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Recent devices -->
|
||||
<VCard title="Recent devices">
|
||||
<VDataTable
|
||||
:items="recentDevices"
|
||||
:headers="recentDeviceHeader"
|
||||
hide-default-footer
|
||||
class="text-no-wrap rounded-0"
|
||||
>
|
||||
<template #item.browser="{ item }">
|
||||
<div class="d-flex text-high-emphasis">
|
||||
<VAvatar
|
||||
:image="item.logo"
|
||||
:size="22"
|
||||
class="me-4"
|
||||
/>
|
||||
{{ item.browser }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Enable One Time Password Dialog -->
|
||||
<TwoFactorAuthDialog
|
||||
v-model:isDialogVisible="isTwoFactorDialogOpen"
|
||||
:sms-code="smsVerificationNumber"
|
||||
/>
|
||||
</template>
|
130
resources/js/views/apps/ecommerce/settings/SettingsCheckout.vue
Normal file
130
resources/js/views/apps/ecommerce/settings/SettingsCheckout.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
const contactMethod = ref('Phone number')
|
||||
const fullName = ref('Only require last name')
|
||||
const companyName = ref('Don\'t include')
|
||||
const addressLine = ref('Optional')
|
||||
const shippingAddress = ref('Optional')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
title="Customer contact method"
|
||||
subtitle="Select what contact method customers use to check out."
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRadioGroup
|
||||
v-model="contactMethod"
|
||||
class="mb-4"
|
||||
>
|
||||
<VRadio
|
||||
label="Phone number"
|
||||
value="Phone number"
|
||||
/>
|
||||
<VRadio
|
||||
label="Email"
|
||||
value="Email"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<VAlert
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
icon="ri-information-line"
|
||||
>
|
||||
<VAlertTitle class="mb-0">
|
||||
To send SMS updates, you need to install an SMS App.
|
||||
</VAlertTitle>
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
title="Customer information"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRadioGroup
|
||||
v-model="fullName"
|
||||
label="Full name"
|
||||
class="mb-4"
|
||||
>
|
||||
<VRadio
|
||||
value="Only require last name"
|
||||
label="Only require last name"
|
||||
/>
|
||||
<VRadio
|
||||
value="Require first and last name"
|
||||
label="Require first and last name"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<VRadioGroup
|
||||
v-model="companyName"
|
||||
label="Company name"
|
||||
class="mb-4"
|
||||
>
|
||||
<VRadio
|
||||
value="Don't include"
|
||||
label="Don't include"
|
||||
/>
|
||||
<VRadio
|
||||
value="Optional"
|
||||
label="Optional"
|
||||
/>
|
||||
<VRadio
|
||||
value="Required"
|
||||
label="Required"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<VRadioGroup
|
||||
v-model="addressLine"
|
||||
label="Address line 2 (apartment, unit, etc.)"
|
||||
class="mb-4"
|
||||
>
|
||||
<VRadio
|
||||
value="Don't include"
|
||||
label="Don't include"
|
||||
/>
|
||||
<VRadio
|
||||
value="Optional"
|
||||
label="Optional"
|
||||
/>
|
||||
<VRadio
|
||||
value="Required"
|
||||
label="Required"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<VRadioGroup
|
||||
v-model="shippingAddress"
|
||||
label="Shipping address phone number"
|
||||
class="mb-4"
|
||||
>
|
||||
<VRadio
|
||||
value="Don't include"
|
||||
label="Don't include"
|
||||
/>
|
||||
<VRadio
|
||||
value="Optional"
|
||||
label="Optional"
|
||||
/>
|
||||
<VRadio
|
||||
value="Required"
|
||||
label="Required"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</template>
|
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 @@
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
title="Location Name"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VTextField
|
||||
label="Location Name"
|
||||
placeholder="Empire Hub"
|
||||
/>
|
||||
<div class="my-4">
|
||||
<VCheckbox label="Fulfil online orders from this location" />
|
||||
</div>
|
||||
<VAlert
|
||||
color="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="28"
|
||||
icon="ri-information-line"
|
||||
variant="elevated"
|
||||
color="info"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
<VAlertTitle class="mb-0">
|
||||
This is your default location. To change whether you fulfill online orders from this location, select another default location first.
|
||||
</VAlertTitle>
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Address">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
label="Country/religion"
|
||||
placeholder="Select Country"
|
||||
:items="['United States', 'UK', 'Canada']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="Address"
|
||||
placeholder="123 , New Street"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="Apartment, suite, etc."
|
||||
placeholder="Empire Heights"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="Phone"
|
||||
placeholder="+1 (234) 456-7890"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="City"
|
||||
placeholder="New York"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="State"
|
||||
placeholder="NY"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="PIN code"
|
||||
placeholder="123897"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4 mt-6">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,192 @@
|
||||
<script setup>
|
||||
const customerNotifications = ref([
|
||||
{
|
||||
type: 'New customer sign up',
|
||||
email: true,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Customer account password reset',
|
||||
email: false,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'Customer account invite',
|
||||
email: false,
|
||||
app: false,
|
||||
},
|
||||
])
|
||||
|
||||
const shippingNotifications = ref([
|
||||
{
|
||||
type: 'Picked up',
|
||||
email: true,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Shipping update ',
|
||||
email: false,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'Delivered',
|
||||
email: false,
|
||||
app: false,
|
||||
},
|
||||
])
|
||||
|
||||
const ordersNotification = ref([
|
||||
{
|
||||
type: 'Order purchase',
|
||||
email: true,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Order cancelled',
|
||||
email: false,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'Order refund request',
|
||||
email: false,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Order confirmation',
|
||||
email: false,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'Payment error',
|
||||
email: false,
|
||||
app: true,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mb-4">
|
||||
<VCardText>
|
||||
<h5 class="text-h5 mb-4">
|
||||
Customer
|
||||
</h5>
|
||||
<VTable class="text-no-wrap text-high-emphasis border rounded mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
TYPE
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
</th>
|
||||
<th scope="col">
|
||||
APP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="notification in customerNotifications"
|
||||
:key="notification.type"
|
||||
>
|
||||
<td width="400px">
|
||||
{{ notification.type }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.email" />
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<VCheckbox v-model="notification.app" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<h5 class="text-h5 mb-4">
|
||||
Orders
|
||||
</h5>
|
||||
<VTable class="border rounded text-high-emphasis text-no-wrap mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
TYPE
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
</th>
|
||||
<th scope="col">
|
||||
APP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="notification in ordersNotification"
|
||||
:key="notification.type"
|
||||
>
|
||||
<td width="400px">
|
||||
{{ notification.type }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.email" />
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<VCheckbox v-model="notification.app" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<h5 class="text-h5 mb-4">
|
||||
Shipping
|
||||
</h5>
|
||||
<VTable class="border rounded text-high-emphasis text-no-wrap mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
TYPE
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
</th>
|
||||
<th scope="col">
|
||||
APP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="notification in shippingNotifications"
|
||||
:key="notification.type"
|
||||
>
|
||||
<td width="400px">
|
||||
{{ notification.type }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.email" />
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<VCheckbox v-model="notification.app" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</template>
|
172
resources/js/views/apps/ecommerce/settings/SettingsPayment.vue
Normal file
172
resources/js/views/apps/ecommerce/settings/SettingsPayment.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import paypal from '@images/cards/paypal-primary.png'
|
||||
|
||||
const isAddPaymentMethodsDialogVisible = ref(false)
|
||||
const isPaymentProvidersDialogVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 👉 Payment Providers -->
|
||||
<VCard
|
||||
class="mb-6"
|
||||
title="Payment providers"
|
||||
>
|
||||
<VCardText>
|
||||
<p>
|
||||
Providers that enable you to accept payment methods at a rate set by the third-party. An additional fee will apply to new orders once you select a plan.
|
||||
</p>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
@click="isPaymentProvidersDialogVisible = !isPaymentProvidersDialogVisible"
|
||||
>
|
||||
Choose a provider
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Supported Payment Methods -->
|
||||
<VCard
|
||||
title="Supported payment methods"
|
||||
subtitle="Payment methods that are available with one of Vuexy's approved payment providers."
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<h6 class="text-h6 mb-5">
|
||||
Default
|
||||
</h6>
|
||||
<div class="rounded bg-var-theme-background pa-5 mb-6">
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<VAvatar
|
||||
variant="elevated"
|
||||
color="#ffffff"
|
||||
rounded
|
||||
class="px-1"
|
||||
>
|
||||
<VImg
|
||||
:src="paypal"
|
||||
height="21"
|
||||
width="21"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<VBtn variant="text">
|
||||
Activate PayPal
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-space-between flex-wrap mt-6 gap-x-4">
|
||||
<div>
|
||||
<div class="text-body-2 mb-2">
|
||||
Provider
|
||||
</div>
|
||||
<h6 class="text-h6">
|
||||
PayPal
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-body-2 mb-2">
|
||||
Status
|
||||
</div>
|
||||
<VChip
|
||||
color="warning"
|
||||
size="small"
|
||||
>
|
||||
Inactive
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-body-2 mb-2">
|
||||
Transaction Fee
|
||||
</div>
|
||||
<h6 class="text-h6">
|
||||
2.99%
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
@click="isAddPaymentMethodsDialogVisible = !isAddPaymentMethodsDialogVisible"
|
||||
>
|
||||
Add Payment Method
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Manual Payment Methods -->
|
||||
<VCard
|
||||
title="Manual payment methods"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<p>Payments that are made outside your online store. When a customer selects a manual payment method such as cash on delivery, you'll need to approve their order before it can be fulfilled.</p>
|
||||
|
||||
<VBtnGroup
|
||||
v-show="$vuetify.display.smAndUp"
|
||||
divided
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
>
|
||||
<VBtn>
|
||||
Add Manual Payment Methods
|
||||
</VBtn>
|
||||
<VBtn>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="ri-arrow-down-s-line"
|
||||
/>
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, index) in ['Create custom payment method', 'Bank Deposit', 'Money Order', 'Cash on Delivery(COD)']"
|
||||
:key="index"
|
||||
:value="index"
|
||||
>
|
||||
<VListItemTitle>{{ item }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</VBtnGroup>
|
||||
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
class="d-block d-sm-none"
|
||||
>
|
||||
Add Manual Payment Methods
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, index) in ['Create custom payment method', 'Bank Deposit', 'Money Order', 'Cash on Delivery(COD)']"
|
||||
:key="index"
|
||||
:value="index"
|
||||
>
|
||||
<VListItemTitle>{{ item }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddPaymentMethodDialog v-model:is-dialog-visible="isAddPaymentMethodsDialogVisible" />
|
||||
<PaymentProvidersDialog v-model:is-dialog-visible="isPaymentProvidersDialogVisible" />
|
||||
</template>
|
@@ -0,0 +1,193 @@
|
||||
<script setup>
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import americaFlag from '@images/icons/countries/us.png'
|
||||
|
||||
const domesticTableData = [
|
||||
{
|
||||
rate: 'Weight',
|
||||
condition: '5Kg-10Kg',
|
||||
price: '$9',
|
||||
},
|
||||
{
|
||||
rate: 'VAT',
|
||||
condition: '12%',
|
||||
price: '$25',
|
||||
},
|
||||
{
|
||||
rate: 'Duty',
|
||||
condition: '-',
|
||||
price: '-',
|
||||
},
|
||||
]
|
||||
|
||||
const InternationalTableData = [
|
||||
{
|
||||
rate: 'Weight',
|
||||
condition: '5Kg-10Kg',
|
||||
price: '$9',
|
||||
},
|
||||
{
|
||||
rate: 'VAT',
|
||||
condition: '12%',
|
||||
price: '$25',
|
||||
},
|
||||
{
|
||||
rate: 'Duty',
|
||||
condition: 'Japan',
|
||||
price: '$49',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mb-6">
|
||||
<VCardItem
|
||||
title="Shipping Zone"
|
||||
subtitle="Choose where you ship and how much you charge for shipping at checkout."
|
||||
>
|
||||
<template #append>
|
||||
<VBtn variant="text">
|
||||
Create Zone
|
||||
</VBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<div class="mb-6">
|
||||
<div class="d-flex flex-wrap align-center mb-4">
|
||||
<VAvatar
|
||||
:image="avatar1"
|
||||
size="34"
|
||||
class="me-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
Domestic
|
||||
</h6>
|
||||
<p class="text-body-2 mb-0">
|
||||
United state of America
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div>
|
||||
<IconBtn size="large">
|
||||
<VIcon icon="ri-pencil-line" />
|
||||
</IconBtn>
|
||||
<IconBtn size="large">
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VTable class="border rounded mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RATE NAME</th>
|
||||
<th>CONDITION</th>
|
||||
<th>PRICE</th>
|
||||
<th>ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(data, index) in domesticTableData"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ data.rate }}</td>
|
||||
<td>{{ data.condition }}</td>
|
||||
<td>{{ data.price }}</td>
|
||||
<td style="inline-size: 2rem;">
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
</IconBtn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Add rate
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex flex-wrap align-center mb-4">
|
||||
<VAvatar
|
||||
:image="americaFlag"
|
||||
size="34"
|
||||
class="me-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
International
|
||||
</h6>
|
||||
<p class="text-body-2 mb-0">
|
||||
United state of America
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div>
|
||||
<IconBtn size="large">
|
||||
<VIcon icon="ri-pencil-line" />
|
||||
</IconBtn>
|
||||
<IconBtn size="large">
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VTable class="border rounded mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RATE NAME</th>
|
||||
<th>CONDITION</th>
|
||||
<th>PRICE</th>
|
||||
<th>ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(data, index) in InternationalTableData"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ data.rate }}</td>
|
||||
<td>{{ data.condition }}</td>
|
||||
<td>{{ data.price }}</td>
|
||||
<td style="inline-size: 2rem;">
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
</IconBtn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Add rate
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<VCard
|
||||
title="Profile"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Store name"
|
||||
placeholder="ABCD"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Phone"
|
||||
placeholder="+(123) 456-7890"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Store contact email"
|
||||
placeholder="johndoe@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Sender email"
|
||||
placeholder="johndoe@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VAlert
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
icon="ri-notification-3-line"
|
||||
>
|
||||
<VAlertTitle class="mb-0">
|
||||
Confirm that you have access to johndoe@gmail.com in sender email settings.
|
||||
</VAlertTitle>
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
title="Billing Information"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Legal business name"
|
||||
placeholder="Themeselection"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
label="Country*"
|
||||
:items="['United States', 'Canada', 'UK']"
|
||||
placeholder="Canada"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
placeholder="126, New Street"
|
||||
label="Address"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Apartment,suit, etc."
|
||||
placeholder="Empire Heights"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="City"
|
||||
placeholder="New York"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="State"
|
||||
placeholder="NY"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
label="PIN Code"
|
||||
placeholder="111011"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
title="Time zone and units of measurement"
|
||||
subtitle="Used to calculate product prices, shipping weights, and order times."
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
label="Time zone"
|
||||
:items="['(UTC-12:00) International Date Line West', '(UTC-11:00) Coordinated Universal Time-11', '(UTC-09:00) Alaska', '(UTC-08:00) Baja California']"
|
||||
placeholder="(UTC-12:00) International Date Line West"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
label="Unit system"
|
||||
:items="['Metric System', 'Imperial', 'International System']"
|
||||
placeholder="Metric System"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
label="Default weight unit"
|
||||
placeholder="Kilogram"
|
||||
:items="['Kilogram', 'Pounds', 'Gram']"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
title="Store currency"
|
||||
subtitle="The currency your products are sold in."
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VSelect
|
||||
label="Store currency"
|
||||
:items="['USD', 'INR', 'Euro', 'Pound']"
|
||||
placeholder="USD"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
title="Order id format"
|
||||
subtitle="Shown on the Orders page, customer pages, and customer order notifications to identify orders."
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Prefix"
|
||||
prefix="#"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Suffix"
|
||||
suffix="$"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div class="mt-2">
|
||||
Your order ID will appear as #1001, #1002, #1003 ...
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<div class="d-flex justify-end gap-x-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn>Save Changes</VBtn>
|
||||
</div>
|
||||
</template>
|
1
resources/js/views/apps/ecommerce/types.js
Normal file
1
resources/js/views/apps/ecommerce/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
302
resources/js/views/apps/email/ComposeDialog.vue
Normal file
302
resources/js/views/apps/email/ComposeDialog.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup>
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import {
|
||||
EditorContent,
|
||||
useEditor,
|
||||
} from '@tiptap/vue-3'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const to = ref('')
|
||||
const cc = ref('')
|
||||
const bcc = ref('')
|
||||
const subject = ref('')
|
||||
const message = ref('')
|
||||
const emailCc = ref(false)
|
||||
const emailBcc = ref(false)
|
||||
|
||||
const resetValues = () => {
|
||||
to.value = subject.value = message.value = ''
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
content: '',
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Image,
|
||||
Placeholder.configure({ placeholder: 'Message' }),
|
||||
Underline,
|
||||
Link.configure({ openOnClick: false }),
|
||||
],
|
||||
})
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.value?.getAttributes('link').href
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null)
|
||||
return
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
const addImage = () => {
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL')
|
||||
if (url)
|
||||
editor.value?.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
class="email-compose-dialog"
|
||||
elevation="24"
|
||||
max-width="30vw"
|
||||
>
|
||||
<VCardItem class="py-3">
|
||||
<VCardTitle class="text-medium-emphasis">
|
||||
Compose Mail
|
||||
</VCardTitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn @click="$emit('close')">
|
||||
<VIcon icon="ri-subtract-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn @click="$emit('close'); resetValues()">
|
||||
<VIcon icon="ri-close-line" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<div class="pe-5">
|
||||
<VTextField
|
||||
v-model="to"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<div class="text-disabled font-weight-medium">
|
||||
To:
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="cursor-pointer text-medium-emphasis">
|
||||
<span @click="emailCc = !emailCc">Cc</span>
|
||||
<span class="mx-1">|</span>
|
||||
<span @click="emailBcc = !emailBcc">Bcc</span>
|
||||
</span>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
|
||||
<VExpandTransition>
|
||||
<div v-if="emailCc">
|
||||
<VDivider />
|
||||
|
||||
<VTextField
|
||||
v-model="cc"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<div class="text-disabled font-weight-medium">
|
||||
Cc:
|
||||
</div>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<VExpandTransition>
|
||||
<div v-if="emailBcc">
|
||||
<VDivider />
|
||||
|
||||
<VTextField
|
||||
v-model="bcc"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<div class="text-disabled font-weight-medium">
|
||||
Bcc:
|
||||
</div>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VTextField
|
||||
v-model="subject"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<div class="text-disabled font-weight-medium">
|
||||
Subject:
|
||||
</div>
|
||||
</template>
|
||||
</VTextField>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Tiptap editor -->
|
||||
<div class="tiptap-editor-wrapper">
|
||||
<div
|
||||
v-if="editor"
|
||||
class="d-flex flex-wrap gap-x-1 px-4 py-2"
|
||||
>
|
||||
<IconBtn
|
||||
rounded
|
||||
:color="editor.isActive('bold') ? 'primary' : ''"
|
||||
:variant="editor.isActive('bold') ? 'tonal' : 'text'"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<VIcon icon="ri-bold" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
:color="editor.isActive('underline') ? 'primary' : ''"
|
||||
:variant="editor.isActive('underline') ? 'tonal' : 'text'"
|
||||
@click="editor.commands.toggleUnderline()"
|
||||
>
|
||||
<VIcon icon="ri-underline" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
:color="editor.isActive('italic') ? 'primary' : ''"
|
||||
:variant="editor.isActive('italic') ? 'tonal' : 'text'"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<VIcon icon="ri-italic" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
:color="editor.isActive('bulletList') ? 'primary' : ''"
|
||||
:variant="editor.isActive('bulletList') ? 'tonal' : 'text'"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
>
|
||||
<VIcon icon="ri-list-check" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
:color="editor.isActive('orderedList') ? 'primary' : ''"
|
||||
:variant="editor.isActive('orderedList') ? 'tonal' : 'text'"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
>
|
||||
<VIcon icon="ri-list-ordered-2" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
@click="setLink"
|
||||
>
|
||||
<VIcon icon="ri-links-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
@click="addImage"
|
||||
>
|
||||
<VIcon icon="ri-image-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="mx-5">
|
||||
<EditorContent :editor="editor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center px-5 py-4 gap-4">
|
||||
<VBtn append-icon="ri-send-plane-line">
|
||||
Send
|
||||
<VMenu activator="parent">
|
||||
<VList :items="['Schedule Mail', 'Save Draft']" />
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-attachment-2" />
|
||||
</IconBtn>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn @click="$emit('close'); resetValues()">
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.email-compose-dialog {
|
||||
z-index: 910 !important;
|
||||
|
||||
.v-field--prepended {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
.v-card-item {
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
||||
}
|
||||
|
||||
.v-textarea .v-field {
|
||||
--v-field-padding-start: 20px;
|
||||
}
|
||||
|
||||
.v-field__outline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
block-size: 150px;
|
||||
overflow-y: auto;
|
||||
padding-block: .5rem;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
p.is-editor-empty:first-child::before {
|
||||
block-size: 0;
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: inline-start;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ul,ol{
|
||||
padding-inline: 1.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
213
resources/js/views/apps/email/EmailLeftSidebarContent.vue
Normal file
213
resources/js/views/apps/email/EmailLeftSidebarContent.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
|
||||
const emit = defineEmits(['toggleComposeDialogVisibility'])
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const folders = [
|
||||
{
|
||||
title: 'Inbox',
|
||||
prependIcon: 'ri-mail-line',
|
||||
to: { name: 'apps-email' },
|
||||
badge: {
|
||||
content: '21',
|
||||
color: 'primary',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Sent',
|
||||
prependIcon: 'ri-send-plane-line',
|
||||
to: {
|
||||
name: 'apps-email-filter',
|
||||
params: { filter: 'sent' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Draft',
|
||||
prependIcon: 'ri-edit-box-line',
|
||||
to: {
|
||||
name: 'apps-email-filter',
|
||||
params: { filter: 'draft' },
|
||||
},
|
||||
badge: {
|
||||
content: '2',
|
||||
color: 'warning',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Starred',
|
||||
prependIcon: 'ri-star-line',
|
||||
to: {
|
||||
name: 'apps-email-filter',
|
||||
params: { filter: 'starred' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Spam',
|
||||
prependIcon: 'ri-spam-2-line',
|
||||
to: {
|
||||
name: 'apps-email-filter',
|
||||
params: { filter: 'spam' },
|
||||
},
|
||||
badge: {
|
||||
content: '4',
|
||||
color: 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Trash',
|
||||
prependIcon: 'ri-delete-bin-7-line',
|
||||
to: {
|
||||
name: 'apps-email-filter',
|
||||
params: { filter: 'trashed' },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const labels = [
|
||||
{
|
||||
title: 'Personal',
|
||||
color: 'success',
|
||||
to: {
|
||||
name: 'apps-email-label',
|
||||
params: { label: 'personal' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
color: 'primary',
|
||||
to: {
|
||||
name: 'apps-email-label',
|
||||
params: { label: 'company' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Important',
|
||||
color: 'warning',
|
||||
to: {
|
||||
name: 'apps-email-label',
|
||||
params: { label: 'important' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Private',
|
||||
color: 'error',
|
||||
to: {
|
||||
name: 'apps-email-label',
|
||||
params: { label: 'private' },
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<!-- 👉 Compose -->
|
||||
<div class="pa-5">
|
||||
<VBtn
|
||||
block
|
||||
@click="$emit('toggleComposeDialogVisibility')"
|
||||
>
|
||||
Compose
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Folders -->
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="h-100 pt-4"
|
||||
>
|
||||
<ul class="email-filters-labels">
|
||||
<RouterLink
|
||||
v-for="folder in folders"
|
||||
:key="folder.title"
|
||||
v-slot="{ isActive, href, navigate }"
|
||||
class="d-flex align-center cursor-pointer"
|
||||
:to="folder.to"
|
||||
custom
|
||||
>
|
||||
<li
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="isActive && 'email-filter-active text-primary'"
|
||||
class="d-flex align-center cursor-pointer"
|
||||
@click="navigate"
|
||||
>
|
||||
<VIcon
|
||||
:icon="folder.prependIcon"
|
||||
class="me-2"
|
||||
size="20"
|
||||
/>
|
||||
<span>{{ folder.title }}</span>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<VChip
|
||||
v-if="folder.badge?.content"
|
||||
size="x-small"
|
||||
:color="folder.badge.color"
|
||||
>
|
||||
{{ folder.badge.content }}
|
||||
</VChip>
|
||||
</li>
|
||||
</RouterLink>
|
||||
|
||||
<!-- 👉 Labels -->
|
||||
<li class="text-sm d-block text-uppercase text-disabled mt-9 mb-4">
|
||||
LABELS
|
||||
</li>
|
||||
<RouterLink
|
||||
v-for="label in labels"
|
||||
:key="label.title"
|
||||
v-slot="{ isActive, href, navigate }"
|
||||
class="d-flex align-center"
|
||||
:to="label.to"
|
||||
custom
|
||||
>
|
||||
<li
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="isActive && 'email-label-active text-primary'"
|
||||
class="cursor-pointer"
|
||||
@click="navigate"
|
||||
>
|
||||
<VIcon
|
||||
:color="label.color"
|
||||
icon="ri-circle-fill"
|
||||
size="12"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ label.title }}</span>
|
||||
</li>
|
||||
</RouterLink>
|
||||
</ul>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.email-filters-labels {
|
||||
> li {
|
||||
position: relative;
|
||||
margin-block-end: 4px;
|
||||
padding-block: 4px;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.email-filter-active,
|
||||
.email-label-active {
|
||||
&::after {
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 3px;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
487
resources/js/views/apps/email/EmailView.vue
Normal file
487
resources/js/views/apps/email/EmailView.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<script setup>
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import {
|
||||
EditorContent,
|
||||
useEditor,
|
||||
} from '@tiptap/vue-3'
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useEmail } from '@/views/apps/email/useEmail'
|
||||
|
||||
const props = defineProps({
|
||||
email: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
emailMeta: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'refresh',
|
||||
'navigated',
|
||||
'close',
|
||||
'trash',
|
||||
'unread',
|
||||
'read',
|
||||
'star',
|
||||
'unstar',
|
||||
])
|
||||
|
||||
const { updateEmailLabels } = useEmail()
|
||||
const { labels, resolveLabelColor, emailMoveToFolderActions, shallShowMoveToActionFor, moveSelectedEmailTo } = useEmail()
|
||||
|
||||
const handleMoveMailsTo = action => {
|
||||
moveSelectedEmailTo(action, [props.email.id])
|
||||
emit('refresh')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const updateMailLabel = async label => {
|
||||
await updateEmailLabels([props.email.id], label)
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
content: '',
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Image,
|
||||
Placeholder.configure({ placeholder: 'Write a Comment...' }),
|
||||
Underline,
|
||||
Link.configure({ openOnClick: false }),
|
||||
],
|
||||
})
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.value?.getAttributes('link').href
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null)
|
||||
return
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
const addImage = () => {
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
const url = window.prompt('URL')
|
||||
if (url)
|
||||
editor.value?.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ℹ️ calc(100% - 256px) => 265px is left sidebar width -->
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
:model-value="!!props.email"
|
||||
location="right"
|
||||
:scrim="false"
|
||||
floating
|
||||
class="email-view"
|
||||
>
|
||||
<template v-if="props.email">
|
||||
<!-- 👉 header -->
|
||||
|
||||
<div class="email-view-header d-flex align-center px-5 py-4">
|
||||
<IconBtn
|
||||
class="me-2 flip-in-rtl"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<VIcon icon="ri-arrow-left-s-line" />
|
||||
</IconBtn>
|
||||
|
||||
<div class="d-flex align-center flex-wrap flex-grow-1 overflow-hidden gap-2">
|
||||
<h6 class="text-h6 font-weight-regular text-truncate">
|
||||
{{ props.email.subject }}
|
||||
</h6>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VChip
|
||||
v-for="label in props.email.labels"
|
||||
:key="label"
|
||||
:color="resolveLabelColor(label)"
|
||||
size="small"
|
||||
class="text-capitalize flex-shrink-0"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-2">
|
||||
<IconBtn
|
||||
variant="plain"
|
||||
:disabled="!props.emailMeta.hasPreviousEmail"
|
||||
class="flip-in-rtl"
|
||||
@click="$emit('navigated', 'previous')"
|
||||
>
|
||||
<VIcon icon="ri-arrow-left-s-line" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
variant="plain"
|
||||
class="flip-in-rtl"
|
||||
:disabled="!props.emailMeta.hasNextEmail"
|
||||
@click="$emit('navigated', 'next')"
|
||||
>
|
||||
<VIcon icon="ri-arrow-right-s-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Action bar -->
|
||||
<div class="email-view-action-bar d-flex align-center text-medium-emphasis gap-1 px-5">
|
||||
<!-- Trash -->
|
||||
<IconBtn
|
||||
v-show="!props.email.isDeleted"
|
||||
@click="$emit('trash'); $emit('close')"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Delete Mail
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Read/Unread -->
|
||||
<IconBtn @click.stop="$emit('unread'); $emit('close')">
|
||||
<VIcon icon="ri-mail-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Mark as Unread
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Move to folder -->
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-folder-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Move to
|
||||
</VTooltip>
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList density="compact">
|
||||
<template
|
||||
v-for="moveTo in emailMoveToFolderActions"
|
||||
:key="moveTo.title"
|
||||
>
|
||||
<VListItem
|
||||
:class="shallShowMoveToActionFor(moveTo.action) ? 'd-flex' : 'd-none'"
|
||||
class="align-center"
|
||||
href="#"
|
||||
@click="handleMoveMailsTo(moveTo.action)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="moveTo.icon"
|
||||
class="me-2"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="text-capitalize">
|
||||
{{ moveTo.action }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Update labels -->
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-price-tag-3-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Label
|
||||
</VTooltip>
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="label in labels"
|
||||
:key="label.title"
|
||||
href="#"
|
||||
@click.stop="updateMailLabel(label.title)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VBadge
|
||||
inline
|
||||
:color="resolveLabelColor(label.title)"
|
||||
dot
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="ms-2 text-capitalize">
|
||||
{{ label.title }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- Star/Unstar -->
|
||||
<IconBtn
|
||||
:color="props.email.isStarred ? 'warning' : 'default'"
|
||||
@click="props.email?.isStarred ? $emit('unstar') : $emit('star')"
|
||||
>
|
||||
<VIcon icon="ri-star-line" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- Dots vertical -->
|
||||
<MoreBtn />
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Mail Content -->
|
||||
<PerfectScrollbar
|
||||
tag="div"
|
||||
class="mail-content-container flex-grow-1"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<VCard class="ma-6 mb-4">
|
||||
<VCardText class="mail-header">
|
||||
<div class="d-flex align-start">
|
||||
<VAvatar
|
||||
size="38"
|
||||
class="me-3"
|
||||
>
|
||||
<VImg
|
||||
:src="props.email.from.avatar"
|
||||
:alt="props.email.from.name"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<div class="d-flex flex-wrap flex-grow-1 overflow-hidden">
|
||||
<div class="text-truncate">
|
||||
<h6 class="text-h6 font-weight-regular text-truncate">
|
||||
{{ props.email.from.name }}
|
||||
</h6>
|
||||
<p class="text-body-2 mb-0">
|
||||
{{ props.email.from.email }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-disabled me-4">{{ formatDate(props.email.time) }}</span>
|
||||
<IconBtn v-show="props.email.attachments.length">
|
||||
<VIcon icon="ri-attachment-2" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
<MoreBtn class="align-self-sm-center" />
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="text-base"
|
||||
v-html="props.email.message"
|
||||
/>
|
||||
<!-- eslint-enable -->
|
||||
</VCardText>
|
||||
|
||||
<template v-if="props.email.attachments.length">
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-column gap-y-4">
|
||||
<span>Attachments</span>
|
||||
<div
|
||||
v-for="attachment in props.email.attachments"
|
||||
:key="attachment.fileName"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<VImg
|
||||
:src="attachment.thumbnail"
|
||||
:alt="attachment.fileName"
|
||||
aspect-ratio="1"
|
||||
max-height="24"
|
||||
max-width="24"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ attachment.fileName }}</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</template>
|
||||
</VCard>
|
||||
|
||||
<VCard class="ma-6">
|
||||
<VCardText>
|
||||
<h6 class="text-h6 font-weight-regular mb-6">
|
||||
Reply to Ross Geller
|
||||
</h6>
|
||||
<!-- 👉 Tiptap editor -->
|
||||
<div class="tiptap-editor-wrapper">
|
||||
<div
|
||||
v-if="editor"
|
||||
class="d-flex flex-wrap gap-x-2 mb-6"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-bold"
|
||||
:color="editor.isActive('bold') ? 'primary' : ''"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('underline') ? 'primary' : ''"
|
||||
icon="ri-underline"
|
||||
size="20"
|
||||
@click="editor.commands.toggleUnderline()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('italic') ? 'primary' : ''"
|
||||
icon="ri-italic"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('bulletList') ? 'primary' : ''"
|
||||
icon="ri-list-check"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('orderedList') ? 'primary' : ''"
|
||||
icon="ri-list-ordered-2"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
icon="ri-links-line"
|
||||
size="20"
|
||||
@click="setLink"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
icon="ri-image-line"
|
||||
size="20"
|
||||
@click="addImage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditorContent :editor="editor" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center justify-end mt-6">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="plain"
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon icon="ri-attachment-2" />
|
||||
<span>Attachments</span>
|
||||
</VBtn>
|
||||
<VBtn>
|
||||
<span>Send</span>
|
||||
<VIcon icon="ri-send-plane-line" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</template>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.email-view {
|
||||
inline-size: 100% !important;
|
||||
|
||||
@media only screen and (min-width: 1280px) {
|
||||
inline-size: calc(100% - 256px) !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 0;
|
||||
block-size: 100px;
|
||||
overflow-y: auto;
|
||||
|
||||
p {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
p.is-editor-empty:first-child::before {
|
||||
block-size: 0;
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: inline-start;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ul,ol{
|
||||
padding-inline: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
border-color: rgba(var(--v-theme-primary), var(--v-border-opacity)) !important;
|
||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.ProseMirror-focused{
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.email-view-action-bar {
|
||||
min-block-size: 54px;
|
||||
}
|
||||
|
||||
.mail-content-container {
|
||||
background-color: rgb(var(--v-theme-on-background), var(--v-hover-opacity));
|
||||
|
||||
.mail-header {
|
||||
min-block-size: 84px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
</style>
|
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: 'ri-mail-line' },
|
||||
{ action: 'spam', icon: 'ri-spam-2-line' },
|
||||
{ action: 'trash', icon: 'ri-delete-bin-line' },
|
||||
]
|
||||
|
||||
const labels = [
|
||||
{
|
||||
title: 'personal',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'company',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'important',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'private',
|
||||
color: 'error',
|
||||
},
|
||||
]
|
||||
|
||||
const resolveLabelColor = label => {
|
||||
if (label === 'personal')
|
||||
return 'success'
|
||||
if (label === 'company')
|
||||
return 'primary'
|
||||
if (label === 'important')
|
||||
return 'warning'
|
||||
if (label === 'private')
|
||||
return 'error'
|
||||
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
const shallShowMoveToActionFor = action => {
|
||||
if (action === 'trash')
|
||||
return route.params.filter !== 'trashed'
|
||||
else if (action === 'inbox')
|
||||
return !(route.params.filter === undefined || route.params.filter === 'sent' || route.params.filter === 'draft')
|
||||
else if (action === 'spam')
|
||||
return !(route.params.filter === 'spam' || route.params.filter === 'sent' || route.params.filter === 'draft')
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const moveSelectedEmailTo = (action, selectedEmails) => {
|
||||
const dataToUpdate = {}
|
||||
if (action === 'inbox') {
|
||||
if (route.params.filter === 'trashed')
|
||||
dataToUpdate.isDeleted = false
|
||||
dataToUpdate.folder = 'inbox'
|
||||
}
|
||||
else if (action === 'spam') {
|
||||
if (route.params.filter === 'trashed')
|
||||
dataToUpdate.isDeleted = false
|
||||
dataToUpdate.folder = 'spam'
|
||||
}
|
||||
else if (action === 'trash') {
|
||||
dataToUpdate.isDeleted = true
|
||||
}
|
||||
updateEmails(selectedEmails, dataToUpdate)
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
resolveLabelColor,
|
||||
shallShowMoveToActionFor,
|
||||
emailMoveToFolderActions,
|
||||
moveSelectedEmailTo,
|
||||
updateEmails,
|
||||
updateEmailLabels,
|
||||
}
|
||||
}
|
126
resources/js/views/apps/invoice/InvoiceAddPaymentDrawer.vue
Normal file
126
resources/js/views/apps/invoice/InvoiceAddPaymentDrawer.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'submit',
|
||||
])
|
||||
|
||||
const invoiceBalance = ref()
|
||||
const paymentAmount = ref()
|
||||
const paymentDate = ref('')
|
||||
const paymentMethod = ref()
|
||||
const paymentNote = ref('')
|
||||
|
||||
const onSubmit = () => {
|
||||
emit('update:isDrawerOpen', false)
|
||||
emit('submit', {
|
||||
invoiceBalance: invoiceBalance.value,
|
||||
paymentAmount: paymentAmount.value,
|
||||
paymentDate: paymentDate.value,
|
||||
paymentMethod: paymentMethod.value,
|
||||
paymentNote: paymentNote.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
location="end"
|
||||
:width="400"
|
||||
:model-value="props.isDrawerOpen"
|
||||
class="scrollable-content"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Add Payment"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="onSubmit">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="invoiceBalance"
|
||||
label="Invoice Balance"
|
||||
type="number"
|
||||
placeholder="$99"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="paymentAmount"
|
||||
label="Payment Amount"
|
||||
type="number"
|
||||
placeholder="$99"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
v-model="paymentDate"
|
||||
label="Payment Date"
|
||||
placeholder="Select Date"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="paymentMethod"
|
||||
label="Select Payment Method"
|
||||
placeholder="Select Payment Method"
|
||||
:items="['Cash', 'Bank Transfer', 'Debit', 'Credit', 'PayPal']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="paymentNote"
|
||||
label="Internal Payment Note"
|
||||
placeholder="Internal Payment Note"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-3"
|
||||
>
|
||||
Send
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
type="reset"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
@click="$emit('update:isDrawerOpen', false)"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
334
resources/js/views/apps/invoice/InvoiceEditable.vue
Normal file
334
resources/js/views/apps/invoice/InvoiceEditable.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<script setup>
|
||||
import InvoiceProductEdit from './InvoiceProductEdit.vue'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'push',
|
||||
'remove',
|
||||
])
|
||||
|
||||
const invoice = ref(props.data.invoice)
|
||||
const salesperson = ref(props.data.salesperson)
|
||||
const thanksNote = ref(props.data.thanksNote)
|
||||
const note = ref(props.data.note)
|
||||
|
||||
// 👉 Clients
|
||||
const clients = ref([])
|
||||
|
||||
// 👉 fetchClients
|
||||
const fetchClients = async () => {
|
||||
const { data, error } = await useApi('/apps/invoice/clients')
|
||||
if (error.value)
|
||||
console.log(error.value)
|
||||
else
|
||||
clients.value = data.value
|
||||
}
|
||||
|
||||
fetchClients()
|
||||
|
||||
// 👉 Add item function
|
||||
const addItem = () => {
|
||||
emit('push', {
|
||||
title: 'App Design',
|
||||
cost: 24,
|
||||
hours: 1,
|
||||
description: 'Designed UI kit & app pages.',
|
||||
})
|
||||
}
|
||||
|
||||
const removeProduct = id => {
|
||||
emit('remove', id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="pa-12">
|
||||
<!-- SECTION Header -->
|
||||
<div class="d-flex flex-wrap justify-space-between flex-column rounded bg-var-theme-background flex-sm-row gap-6 pa-6 mb-6">
|
||||
<!-- 👉 Left Content -->
|
||||
<div>
|
||||
<div class="d-flex align-center mb-6">
|
||||
<!-- 👉 Logo -->
|
||||
<VNodeRenderer
|
||||
:nodes="themeConfig.app.logo"
|
||||
class="me-3"
|
||||
/>
|
||||
|
||||
<!-- 👉 Title -->
|
||||
<h6 class="font-weight-medium text-xl text-uppercase">
|
||||
{{ themeConfig.app.title }}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Address -->
|
||||
<p class="text-high-emphasis mb-0">
|
||||
Office 149, 450 South Brand Brooklyn
|
||||
</p>
|
||||
<p class="text-high-emphasis mb-0">
|
||||
San Diego County, CA 91905, USA
|
||||
</p>
|
||||
<p class="text-high-emphasis mb-0">
|
||||
+1 (123) 456 7891, +44 (876) 543 2198
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Right Content -->
|
||||
<div>
|
||||
<!-- 👉 Invoice Id -->
|
||||
<div class="d-flex align-start font-weight-medium justify-sm-end flex-column flex-sm-row text-lg mb-3">
|
||||
<span
|
||||
class="text-high-emphasis me-4"
|
||||
style="inline-size: 5.625rem ;"
|
||||
>Invoice:</span>
|
||||
<span>
|
||||
<VTextField
|
||||
v-model="invoice.id"
|
||||
disabled
|
||||
density="compact"
|
||||
prefix="#"
|
||||
style="inline-size: 9.5rem;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Issue Date -->
|
||||
<div class="d-flex align-start justify-sm-end flex-column flex-sm-row mb-3">
|
||||
<span
|
||||
class="text-high-emphasis me-4"
|
||||
style="inline-size: 5.625rem;"
|
||||
>Date Issued:</span>
|
||||
|
||||
<span style="inline-size: 9.5rem;">
|
||||
<AppDateTimePicker
|
||||
v-model="invoice.issuedDate"
|
||||
density="compact"
|
||||
placeholder="YYYY-MM-DD"
|
||||
:config="{ position: 'auto right' }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Due Date -->
|
||||
<div class="d-flex align-start justify-sm-end flex-column flex-sm-row mb-0">
|
||||
<span
|
||||
class="text-high-emphasis me-4"
|
||||
style="inline-size: 5.625rem;"
|
||||
>Due Date:</span>
|
||||
<span style="min-inline-size: 9.5rem;">
|
||||
<AppDateTimePicker
|
||||
v-model="invoice.dueDate"
|
||||
density="compact"
|
||||
placeholder="YYYY-MM-DD"
|
||||
:config="{ position: 'auto right' }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<VRow>
|
||||
<VCol class="text-no-wrap">
|
||||
<h6 class="text-h6 mb-4">
|
||||
Invoice To:
|
||||
</h6>
|
||||
|
||||
<VSelect
|
||||
v-model="invoice.client"
|
||||
:items="clients"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
placeholder="Select Client"
|
||||
return-object
|
||||
class="mb-4"
|
||||
style="inline-size: 11.875rem;"
|
||||
/>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.name }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.company }}
|
||||
</p>
|
||||
<p
|
||||
v-if="invoice.client.address"
|
||||
class="mb-0"
|
||||
>
|
||||
{{ invoice.client.address }}, {{ invoice.client.country }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.contact }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.companyEmail }}
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<VCol class="text-no-wrap">
|
||||
<h6 class="text-h6 mb-4">
|
||||
Bill To:
|
||||
</h6>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Total Due:
|
||||
</td>
|
||||
<td>{{ props.data.paymentDetails.totalDue }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Bank Name:
|
||||
</td>
|
||||
<td>{{ props.data.paymentDetails.bankName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Country:
|
||||
</td>
|
||||
<td>{{ props.data.paymentDetails.country }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
IBAN:
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-wrap me-4">
|
||||
{{ props.data.paymentDetails.iban }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
SWIFT Code:
|
||||
</td>
|
||||
<td>{{ props.data.paymentDetails.swiftCode }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-6 border-dashed" />
|
||||
<!-- 👉 Add purchased products -->
|
||||
<div class="add-products-form">
|
||||
<div
|
||||
v-for="(product, index) in props.data.purchasedProducts"
|
||||
:key="product.title"
|
||||
class="mb-4"
|
||||
>
|
||||
<InvoiceProductEdit
|
||||
:id="index"
|
||||
:data="product"
|
||||
@remove-product="removeProduct"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
size="small"
|
||||
prepend-icon="ri-add-line"
|
||||
@click="addItem"
|
||||
>
|
||||
Add Item
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VDivider class="my-6 border-dashed" />
|
||||
|
||||
<!-- 👉 Total Amount -->
|
||||
<div class="d-flex justify-space-between flex-wrap flex-column flex-sm-row">
|
||||
<div class="mb-6 mb-sm-0">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<h6 class="text-h6 me-2">
|
||||
Salesperson:
|
||||
</h6>
|
||||
<VTextField
|
||||
v-model="salesperson"
|
||||
style="inline-size: 8rem;"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VTextField
|
||||
v-model="thanksNote"
|
||||
placeholder="Thanks for your business"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<table class="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Subtotal:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-h6">
|
||||
$1800
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Discount:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-h6">
|
||||
$28
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Tax:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-h6">
|
||||
21%
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<VDivider class="mt-4 mb-3" />
|
||||
|
||||
<table class="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Total:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-h6">
|
||||
$1690
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="my-6 border-dashed" />
|
||||
|
||||
<div>
|
||||
<h6 class="text-h6 mb-2">
|
||||
Note:
|
||||
</h6>
|
||||
<VTextarea
|
||||
v-model="note"
|
||||
placeholder="Write note here..."
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
207
resources/js/views/apps/invoice/InvoiceProductEdit.vue
Normal file
207
resources/js/views/apps/invoice/InvoiceProductEdit.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
title: 'App Design',
|
||||
cost: 24,
|
||||
hours: 1,
|
||||
description: 'Designed UI kit & app pages.',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'removeProduct',
|
||||
'totalAmount',
|
||||
])
|
||||
|
||||
const itemsOptions = [
|
||||
{
|
||||
title: 'App Design',
|
||||
cost: 24,
|
||||
hours: 1,
|
||||
description: 'Designed UI kit & app pages.',
|
||||
},
|
||||
{
|
||||
title: 'App Customization',
|
||||
cost: 26,
|
||||
hours: 1,
|
||||
description: 'Customization & Bug Fixes.',
|
||||
},
|
||||
{
|
||||
title: 'ABC Template',
|
||||
cost: 28,
|
||||
hours: 1,
|
||||
description: 'Vuetify admin template.',
|
||||
},
|
||||
{
|
||||
title: 'App Development',
|
||||
cost: 32,
|
||||
hours: 1,
|
||||
description: 'Native App Development.',
|
||||
},
|
||||
]
|
||||
|
||||
const selectedItem = ref('App Customization')
|
||||
const localProductData = ref(structuredClone(toRaw(props.data)))
|
||||
|
||||
watch(selectedItem, () => {
|
||||
const item = itemsOptions.filter(obj => {
|
||||
return obj.title === selectedItem.value
|
||||
})
|
||||
|
||||
localProductData.value = item[0]
|
||||
})
|
||||
|
||||
const removeProduct = () => {
|
||||
emit('removeProduct', props.id)
|
||||
}
|
||||
|
||||
const totalPrice = computed(() => Number(localProductData.value.cost) * Number(localProductData.value.hours))
|
||||
|
||||
watch(totalPrice, () => {
|
||||
emit('totalAmount', totalPrice.value)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<div class="add-products-header mb-2 d-none d-md-flex mb-4">
|
||||
<VRow class="me-10">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<h6 class="text-h6">
|
||||
Item
|
||||
</h6>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<h6 class="text-h6 ps-2">
|
||||
Cost
|
||||
</h6>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<h6 class="text-h6 ps-2">
|
||||
Hours
|
||||
</h6>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
>
|
||||
<h6 class="text-h6">
|
||||
Price
|
||||
</h6>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
class="d-flex flex-sm-row flex-column-reverse"
|
||||
>
|
||||
<!-- 👉 Left Form -->
|
||||
<div class="pa-5 flex-grow-1">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedItem"
|
||||
:items="itemsOptions"
|
||||
item-title="title"
|
||||
item-value="title"
|
||||
label="Select Item"
|
||||
placeholder="Select Item"
|
||||
class="mb-5"
|
||||
/>
|
||||
|
||||
<VTextarea
|
||||
v-model="localProductData.description"
|
||||
rows="2"
|
||||
label="Description"
|
||||
placeholder="Item description"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
sm="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="localProductData.cost"
|
||||
type="number"
|
||||
label="Cost"
|
||||
placeholder="100"
|
||||
/>
|
||||
|
||||
<div class="text-high-emphasis mt-4">
|
||||
<p class="mb-1">
|
||||
Discount
|
||||
</p>
|
||||
<span>0%</span>
|
||||
<span class="mx-2">
|
||||
0%
|
||||
<VTooltip activator="parent">Tax 1</VTooltip>
|
||||
</span>
|
||||
<span>
|
||||
0%
|
||||
<VTooltip activator="parent">Tax 2</VTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
sm="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="localProductData.hours"
|
||||
type="number"
|
||||
label="Hours"
|
||||
placeholder="5"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="2"
|
||||
sm="4"
|
||||
>
|
||||
<p class="my-2">
|
||||
<span class="d-inline d-md-none">Price: </span>
|
||||
<span class="text-high-emphasis">${{ totalPrice }}</span>
|
||||
</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Item Actions -->
|
||||
<div
|
||||
class="d-flex flex-column align-end item-actions"
|
||||
:class="$vuetify.display.smAndUp ? 'border-s' : 'border-b' "
|
||||
>
|
||||
<IconBtn @click="removeProduct">
|
||||
<VIcon
|
||||
:size="20"
|
||||
icon="ri-close-line"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
133
resources/js/views/apps/invoice/InvoiceSendInvoiceDrawer.vue
Normal file
133
resources/js/views/apps/invoice/InvoiceSendInvoiceDrawer.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'submit',
|
||||
])
|
||||
|
||||
const emailFrom = ref('shelbyComapny@email.com')
|
||||
const emailTo = ref('qConsolidated@email.com')
|
||||
const invoiceSubject = ref('Invoice of purchased Admin Templates')
|
||||
|
||||
const paymentMessage = ref(`Dear Queen Consolidated,
|
||||
|
||||
Thank you for your business, always a pleasure to work with you!
|
||||
|
||||
We have generated a new invoice in the amount of $95.59
|
||||
|
||||
We would appreciate payment of this invoice by 05/11/2019`)
|
||||
|
||||
const onSubmit = () => {
|
||||
emit('update:isDrawerOpen', false)
|
||||
emit('submit', {
|
||||
emailFrom: emailFrom.value,
|
||||
emailTo: emailTo.value,
|
||||
invoiceSubject: invoiceSubject.value,
|
||||
paymentMessage: paymentMessage.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
location="end"
|
||||
:width="400"
|
||||
:model-value="props.isDrawerOpen"
|
||||
class="scrollable-content"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Send Invoice"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="onSubmit">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="emailFrom"
|
||||
label="From"
|
||||
placeholder="sender@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="emailTo"
|
||||
label="To"
|
||||
placeholder="receiver@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="invoiceSubject"
|
||||
label="Subject"
|
||||
placeholder="Invoice of purchased Admin Templates"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="paymentMessage"
|
||||
rows="10"
|
||||
label="Message"
|
||||
placeholder="Thank you for your business, always a pleasure to work with you!"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="mb-6">
|
||||
<VChip
|
||||
label
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
icon="ri-links-line"
|
||||
/>
|
||||
Invoice Attached
|
||||
</VChip>
|
||||
</div>
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-3"
|
||||
>
|
||||
Send
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
@click="$emit('update:isDrawerOpen', false)"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
1
resources/js/views/apps/invoice/types.js
Normal file
1
resources/js/views/apps/invoice/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
110
resources/js/views/apps/logistics/LogisticsCardStatistics.vue
Normal file
110
resources/js/views/apps/logistics/LogisticsCardStatistics.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
const logisticData = ref([
|
||||
{
|
||||
icon: 'ri-car-line',
|
||||
color: 'primary',
|
||||
title: 'On route vehicles',
|
||||
value: 42,
|
||||
change: 18.2,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-alert-line',
|
||||
color: 'warning',
|
||||
title: 'Vehicles with errors',
|
||||
value: 8,
|
||||
change: -8.7,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-stackshare-line',
|
||||
color: 'error',
|
||||
title: 'Deviated from route',
|
||||
value: 27,
|
||||
change: 4.3,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-timer-line',
|
||||
color: 'info',
|
||||
title: 'Late vehicles',
|
||||
value: 13,
|
||||
change: -2.5,
|
||||
isHover: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="(data, index) in logisticData"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="3"
|
||||
sm="6"
|
||||
>
|
||||
<div>
|
||||
<VCard
|
||||
class="logistics-card-statistics cursor-pointer"
|
||||
:style="data.isHover ? `border-block-end-color: rgb(var(--v-theme-${data.color}))` : `border-block-end-color: rgba(var(--v-theme-${data.color}),0.7)`"
|
||||
@mouseenter="data.isHover = true"
|
||||
@mouseleave="data.isHover = false"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center gap-x-4 mb-2">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
:color="data.color"
|
||||
rounded
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="24"
|
||||
/>
|
||||
</VAvatar>
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
</div>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ data.title }}
|
||||
</h6>
|
||||
<div class="d-flex align-center">
|
||||
<div class="text-body-1 font-weight-medium me-2">
|
||||
{{ data.change }}%
|
||||
</div>
|
||||
<span class="text-sm text-disabled">than last week</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@core-scss/base/mixins" as mixins;
|
||||
|
||||
.logistics-card-statistics {
|
||||
border-block-end-style: solid;
|
||||
border-block-end-width: 2px;
|
||||
|
||||
&:hover {
|
||||
border-block-end-width: 3px;
|
||||
margin-block-end: -1px;
|
||||
|
||||
@include mixins.elevation(10);
|
||||
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.skin--bordered{
|
||||
.logistics-card-statistics {
|
||||
&:hover {
|
||||
margin-block-end: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
const chartColors = {
|
||||
donut: {
|
||||
series1: '#56ca00',
|
||||
series2: '#56ca00cc',
|
||||
series3: '#56ca0099',
|
||||
series4: '#56ca0066',
|
||||
},
|
||||
}
|
||||
|
||||
const headingColor = 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))'
|
||||
const labelColor = 'rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity))'
|
||||
|
||||
const deliveryExceptionsChartSeries = [
|
||||
13,
|
||||
25,
|
||||
22,
|
||||
40,
|
||||
]
|
||||
|
||||
const deliveryExceptionsChartConfig = {
|
||||
labels: [
|
||||
'Incorrect address',
|
||||
'Weather conditions',
|
||||
'Federal Holidays',
|
||||
'Damage during transit',
|
||||
],
|
||||
colors: [
|
||||
chartColors.donut.series1,
|
||||
chartColors.donut.series2,
|
||||
chartColors.donut.series3,
|
||||
chartColors.donut.series4,
|
||||
],
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
formatter(val) {
|
||||
return `${ Number.parseInt(val) }%`
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
offsetY: 10,
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
},
|
||||
fontSize: '13px',
|
||||
fontWeight: 400,
|
||||
labels: {
|
||||
colors: headingColor,
|
||||
useSeriesColors: false,
|
||||
},
|
||||
},
|
||||
tooltip: { theme: false },
|
||||
grid: { padding: { top: 15 } },
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '75%',
|
||||
labels: {
|
||||
show: true,
|
||||
value: {
|
||||
fontSize: '26px',
|
||||
color: headingColor,
|
||||
fontWeight: 500,
|
||||
offsetY: -15,
|
||||
formatter(val) {
|
||||
return `${ Number.parseInt(val) }%`
|
||||
},
|
||||
},
|
||||
name: { offsetY: 30 },
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: '1rem',
|
||||
label: 'AVG. Exceptions',
|
||||
color: labelColor,
|
||||
formatter() {
|
||||
return '30%'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [{
|
||||
breakpoint: 420,
|
||||
options: { chart: { height: 400 } },
|
||||
}],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="Delivery exceptions">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VueApexCharts
|
||||
type="donut"
|
||||
height="400"
|
||||
:options="deliveryExceptionsChartConfig"
|
||||
:series="deliveryExceptionsChartSeries"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
const deliveryData = [
|
||||
{
|
||||
title: 'Packages in transit',
|
||||
value: '10k',
|
||||
change: 25.8,
|
||||
icon: 'ri-gift-line',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Packages out for delivery',
|
||||
value: '5k',
|
||||
change: 4.3,
|
||||
icon: 'ri-car-line',
|
||||
color: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Packages delivered',
|
||||
value: '15k',
|
||||
change: -12.5,
|
||||
icon: 'ri-check-line',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Delivery success rate',
|
||||
value: '95%',
|
||||
change: 35.6,
|
||||
icon: 'ri-home-line',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Average delivery time',
|
||||
value: '2.5 Days',
|
||||
change: -2.15,
|
||||
icon: 'ri-timer-line',
|
||||
color: 'secondary',
|
||||
},
|
||||
{
|
||||
title: 'Customer satisfaction',
|
||||
value: '4.5/5',
|
||||
change: 5.7,
|
||||
icon: 'ri-user-line',
|
||||
color: 'error',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem
|
||||
title="Delivery performance"
|
||||
subtitle="12% increase in this month"
|
||||
>
|
||||
<template #append>
|
||||
<MoreBtn class="mt-n5" />
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="(data, index) in deliveryData"
|
||||
:key="index"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
:color="data.color"
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="42"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="26"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle>{{ data.title }}</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
<div
|
||||
:class="data.change > 0 ? 'text-success' : 'text-error'"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.change > 0 ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'"
|
||||
size="24"
|
||||
class="me-1"
|
||||
/>
|
||||
<span>{{ data.change }}%</span>
|
||||
</div>
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<span class="text-high-emphasis text-body-1 font-weight-medium">
|
||||
{{ data.value }}
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
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>
|
||||
<VCardItem
|
||||
title="Orders by countries"
|
||||
subtitle="62 deliveries in progress"
|
||||
>
|
||||
<template #append>
|
||||
<MoreBtn class="mt-n5" />
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VTabs
|
||||
v-model="currentTab"
|
||||
grow
|
||||
class="disable-tab-transition"
|
||||
>
|
||||
<VTab
|
||||
v-for="(tab, index) in tabsData"
|
||||
:key="index"
|
||||
>
|
||||
{{ tab }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VCardText>
|
||||
<VWindow v-model="currentTab">
|
||||
<VWindowItem>
|
||||
<div>
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Myrtle Ullrich
|
||||
</div>
|
||||
<div class="text-body-2 mb-1">
|
||||
101 Boulder, California(CA), 95959
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Barry Schowalter
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
939 Orange, California(CA), 92118
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
|
||||
<VDivider
|
||||
class="my-4"
|
||||
style="border-style: dashed;"
|
||||
/>
|
||||
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="text-body-2 mb-1">
|
||||
162 Windsor, California(CA), 95492
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Helen Jacobs
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
487 Sunset, California(CA), 94043
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<div>
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Barry Schowalter
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
939 Orange, California(CA), 92118
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Myrtle Ullrich
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
101 Boulder, California(CA), 95959
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
|
||||
<VDivider
|
||||
class="my-4"
|
||||
style="border-style: dashed;"
|
||||
/>
|
||||
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
162 Windsor, California(CA), 95492
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Helen Jacobs
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
487 Sunset, California(CA), 94043
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<div>
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Myrtle Ullrich
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
101 Boulder, California(CA), 95959
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Barry Schowalter
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
939 Orange, California(CA), 92118
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
|
||||
<VDivider
|
||||
class="my-4"
|
||||
style="border-style: dashed;"
|
||||
/>
|
||||
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
Sender
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
162 Windsor, California(CA), 95492
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgba(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-primary text-uppercase">
|
||||
Receiver
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Helen Jacobs
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
487 Sunset, California(CA), 94043
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
187
resources/js/views/apps/logistics/LogisticsOverviewTable.vue
Normal file
187
resources/js/views/apps/logistics/LogisticsOverviewTable.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
const itemsPerPage = ref(5)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const { data: vehiclesData } = await useApi(createUrl('/apps/logistics/vehicles', {
|
||||
query: {
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const vehicles = computed(() => vehiclesData.value.vehicles)
|
||||
const totalVehicles = computed(() => vehiclesData.value.totalVehicles)
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'LOCATION',
|
||||
key: 'location',
|
||||
},
|
||||
{
|
||||
title: 'STARTING ROUTE',
|
||||
key: 'startRoute',
|
||||
},
|
||||
{
|
||||
title: 'ENDING ROUTE',
|
||||
key: 'endRoute',
|
||||
},
|
||||
{
|
||||
title: 'WARNINGS',
|
||||
key: 'warnings',
|
||||
},
|
||||
{
|
||||
title: 'PROGRESS',
|
||||
key: 'progress',
|
||||
},
|
||||
]
|
||||
|
||||
const resolveChipColor = warning => {
|
||||
if (warning === 'No Warnings')
|
||||
return 'success'
|
||||
if (warning === 'fuel problems')
|
||||
return 'primary'
|
||||
if (warning === 'Temperature Not Optimal')
|
||||
return 'warning'
|
||||
if (warning === 'Ecu Not Responding')
|
||||
return 'error'
|
||||
if (warning === 'Oil Leakage')
|
||||
return 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="On Route vehicles">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="[
|
||||
{ value: 5, title: '5' },
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 20, title: '20' },
|
||||
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
|
||||
]"
|
||||
:items-length="totalVehicles"
|
||||
:items="vehicles"
|
||||
item-value="location"
|
||||
:headers="headers"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.location="{ item }">
|
||||
<div class="py-2">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
class="me-4"
|
||||
color="secondary"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-bus-line"
|
||||
size="28"
|
||||
/>
|
||||
</VAvatar>
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-logistics-fleet' }"
|
||||
class="text-base text-high-emphasis"
|
||||
>
|
||||
VOL-{{ item.location }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.startRoute="{ item }">
|
||||
{{ item.startCity }}, {{ item.startCountry }}
|
||||
</template>
|
||||
|
||||
<template #item.endRoute="{ item }">
|
||||
{{ item.endCity }}, {{ item.endCountry }}
|
||||
</template>
|
||||
|
||||
<template #item.warnings="{ item }">
|
||||
<VChip
|
||||
:color="resolveChipColor(item.warnings)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.warnings }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.progress="{ item }">
|
||||
<div
|
||||
class="d-flex align-center gap-x-4"
|
||||
style="min-inline-size: 240px;"
|
||||
>
|
||||
<div class="w-100">
|
||||
<VProgressLinear
|
||||
:model-value="item.progress"
|
||||
rounded
|
||||
color="primary"
|
||||
:height="8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{{ item.progress }}%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalVehicles) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalVehicles / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalVehicles / itemsPerPage) ? page = Math.ceil(totalVehicles / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,256 @@
|
||||
<script setup>
|
||||
const chartColors = {
|
||||
line: {
|
||||
series1: '#FFB400',
|
||||
series2: '#9055FD',
|
||||
},
|
||||
}
|
||||
|
||||
const headingColor = 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))'
|
||||
const labelColor = 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))'
|
||||
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Shipment',
|
||||
type: 'column',
|
||||
data: [
|
||||
38,
|
||||
45,
|
||||
33,
|
||||
38,
|
||||
32,
|
||||
50,
|
||||
48,
|
||||
40,
|
||||
42,
|
||||
37,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Delivery',
|
||||
type: 'line',
|
||||
data: [
|
||||
23,
|
||||
28,
|
||||
23,
|
||||
32,
|
||||
28,
|
||||
44,
|
||||
32,
|
||||
38,
|
||||
26,
|
||||
34,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const shipmentConfig = {
|
||||
chart: {
|
||||
type: 'line',
|
||||
stacked: false,
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
},
|
||||
markers: {
|
||||
size: 5,
|
||||
colors: '#fff',
|
||||
strokeColors: chartColors.line.series2,
|
||||
hover: { size: 6 },
|
||||
borderRadius: 4,
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: [
|
||||
0,
|
||||
3,
|
||||
],
|
||||
lineCap: 'round',
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
offsetX: -3,
|
||||
},
|
||||
height: 40,
|
||||
offsetY: 10,
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
vertical: 0,
|
||||
},
|
||||
fontSize: '15px',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 400,
|
||||
labels: {
|
||||
colors: headingColor,
|
||||
useSeriesColors: !1,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 8,
|
||||
borderColor,
|
||||
xaxis: { lines: { show: false } },
|
||||
},
|
||||
colors: [
|
||||
chartColors.line.series1,
|
||||
chartColors.line.series2,
|
||||
],
|
||||
fill: {
|
||||
opacity: [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '30%',
|
||||
borderRadius: 4,
|
||||
borderRadiusApplication: 'around',
|
||||
},
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
xaxis: {
|
||||
tickAmount: 10,
|
||||
categories: [
|
||||
'1 Jan',
|
||||
'2 Jan',
|
||||
'3 Jan',
|
||||
'4 Jan',
|
||||
'5 Jan',
|
||||
'6 Jan',
|
||||
'7 Jan',
|
||||
'8 Jan',
|
||||
'9 Jan',
|
||||
'10 Jan',
|
||||
],
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
offsetY: 8,
|
||||
},
|
||||
axisTicks: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 4,
|
||||
min: 10,
|
||||
max: 50,
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 400,
|
||||
},
|
||||
formatter(val) {
|
||||
return `${ val }%`
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1400,
|
||||
options: {
|
||||
chart: { height: 310 },
|
||||
xaxis: { labels: { style: { fontSize: '10px' } } },
|
||||
legend: {
|
||||
itemMargin: {
|
||||
vertical: 0,
|
||||
horizontal: 10,
|
||||
},
|
||||
fontSize: '13px',
|
||||
offsetY: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1025,
|
||||
options: {
|
||||
chart: { height: 415 },
|
||||
plotOptions: { bar: { columnWidth: '50%' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 982,
|
||||
options: { plotOptions: { bar: { columnWidth: '30%' } } },
|
||||
},
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
chart: { height: 250 },
|
||||
legend: { offsetY: 7 },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem
|
||||
title="Shipment statistics"
|
||||
subtitle="Total number of deliveries 23.8k"
|
||||
>
|
||||
<template #append>
|
||||
<VBtnGroup
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
divided
|
||||
>
|
||||
<VBtn color="primary">
|
||||
January
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon="ri-arrow-down-s-line"
|
||||
color="primary"
|
||||
/>
|
||||
</VBtnGroup>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VueApexCharts
|
||||
id="shipment-statistics"
|
||||
height="320"
|
||||
:options="shipmentConfig"
|
||||
:series="series"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
|
||||
.v-btn-group--divided .v-btn:not(:last-child) {
|
||||
border-inline-end-color: rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
|
||||
#shipment-statistics {
|
||||
.apexcharts-legend-text {
|
||||
font-size: 15px !important;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.apexcharts-legend{
|
||||
.apexcharts-legend-series {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||
border-radius: 0.375rem;
|
||||
block-size: 83%;
|
||||
padding-block: 4px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
154
resources/js/views/apps/logistics/LogisticsVehicleOverview.vue
Normal file
154
resources/js/views/apps/logistics/LogisticsVehicleOverview.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup>
|
||||
const vehicleData = [
|
||||
{
|
||||
icon: 'ri-car-line',
|
||||
title: 'On the way',
|
||||
time: '2hr 10min',
|
||||
percentage: 39.7,
|
||||
color: {
|
||||
bg: 'rgba(var(--v-theme-on-surface), var(--v-hover-opacity))',
|
||||
text: 'rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'ri-download-cloud-2-line',
|
||||
title: 'Unloading',
|
||||
time: '3hr 15min',
|
||||
percentage: 28.3,
|
||||
color: {
|
||||
bg: 'rgb(var(--v-theme-primary))',
|
||||
text: 'rgb(var(--v-theme-on-primary))',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'ri-upload-line',
|
||||
title: 'Loading',
|
||||
time: '1hr 24min',
|
||||
percentage: 17.4,
|
||||
color: {
|
||||
bg: 'rgb(var(--v-theme-info))',
|
||||
text: 'rgb(var(--v-theme-on-primary))',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'ri-timer-line',
|
||||
title: 'Waiting',
|
||||
time: '5hr 19min',
|
||||
percentage: 14.6,
|
||||
color: {
|
||||
bg: 'rgb(var(--v-tooltip-background))',
|
||||
text: 'rgba(var(--v-theme-surface))',
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem title="Vehicles Overview">
|
||||
<template #append>
|
||||
<MoreBtn />
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<div class="d-flex mb-6">
|
||||
<div
|
||||
v-for="(item, index) in vehicleData"
|
||||
:key="item.title"
|
||||
:style="`inline-size: ${item.percentage}%;`"
|
||||
>
|
||||
<div class="vehicle-progress-label position-relative mb-4 text-body-1 d-none d-sm-block">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<VProgressLinear
|
||||
:color="item.color.bg"
|
||||
model-value="100"
|
||||
height="46"
|
||||
:class="index === 0 ? 'rounded-s' : index === vehicleData.length - 1 ? 'rounded-e' : ''"
|
||||
>
|
||||
<p
|
||||
class="text-sm font-weight-medium mb-0"
|
||||
:style="`color: ${item.color.text}`"
|
||||
>
|
||||
{{ item.percentage }}%
|
||||
</p>
|
||||
</VProgressLinear>
|
||||
</div>
|
||||
</div>
|
||||
<VTable class="text-no-wrap">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(vehicle, index) in vehicleData"
|
||||
:key="index"
|
||||
>
|
||||
<td
|
||||
width="70%"
|
||||
class="ps-0"
|
||||
>
|
||||
<div class="d-flex align-center text-high-emphasis">
|
||||
<VIcon
|
||||
:icon="vehicle.icon"
|
||||
size="24"
|
||||
class="me-2"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ vehicle.title }}
|
||||
</h6>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<h6 class="text-h6">
|
||||
{{ vehicle.time }}
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-body-1">{{ vehicle.percentage }}%</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vehicle-progress-label {
|
||||
padding-block-end: 1rem;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
block-size: 10px;
|
||||
content: "";
|
||||
inline-size: 2px;
|
||||
inset-block-end: 0;
|
||||
inset-inline-start: 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
inset-inline: unset 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.v-progress-linear__content {
|
||||
justify-content: start;
|
||||
padding-inline-start: 1rem;
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.v-progress-linear__content {
|
||||
padding-inline-start: 0.75rem !important;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.v-progress-linear__content {
|
||||
padding-inline-start: 0.3rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
326
resources/js/views/apps/roles/RoleCards.vue
Normal file
326
resources/js/views/apps/roles/RoleCards.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<script setup>
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar10 from '@images/avatars/avatar-10.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar7 from '@images/avatars/avatar-7.png'
|
||||
import avatar8 from '@images/avatars/avatar-8.png'
|
||||
import avatar9 from '@images/avatars/avatar-9.png'
|
||||
import poseM from '@images/pages/pose_m1.png'
|
||||
|
||||
// 👉 Roles List
|
||||
const roles = ref([
|
||||
{
|
||||
role: 'Administrator',
|
||||
users: [
|
||||
avatar1,
|
||||
avatar2,
|
||||
avatar3,
|
||||
avatar4,
|
||||
],
|
||||
details: {
|
||||
name: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
name: 'User Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: 'Disputes Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: 'API Control',
|
||||
read: true,
|
||||
write: true,
|
||||
create: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'Manager',
|
||||
users: [
|
||||
avatar1,
|
||||
avatar2,
|
||||
avatar3,
|
||||
avatar4,
|
||||
avatar5,
|
||||
avatar6,
|
||||
avatar7,
|
||||
],
|
||||
details: {
|
||||
name: 'Manager',
|
||||
permissions: [
|
||||
{
|
||||
name: 'Reporting',
|
||||
read: true,
|
||||
write: true,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Payroll',
|
||||
read: true,
|
||||
write: true,
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: 'User Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'Users',
|
||||
users: [
|
||||
avatar1,
|
||||
avatar2,
|
||||
avatar3,
|
||||
avatar4,
|
||||
avatar5,
|
||||
],
|
||||
details: {
|
||||
name: 'Users',
|
||||
permissions: [
|
||||
{
|
||||
name: 'User Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Content Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Disputes Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Database Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'Support',
|
||||
users: [
|
||||
avatar1,
|
||||
avatar2,
|
||||
avatar3,
|
||||
avatar4,
|
||||
avatar5,
|
||||
avatar6,
|
||||
],
|
||||
details: {
|
||||
name: 'Support',
|
||||
permissions: [
|
||||
{
|
||||
name: 'Repository Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Content Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Database Management',
|
||||
read: true,
|
||||
write: true,
|
||||
create: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'Restricted User',
|
||||
users: [
|
||||
avatar1,
|
||||
avatar2,
|
||||
avatar3,
|
||||
avatar4,
|
||||
avatar5,
|
||||
avatar6,
|
||||
avatar7,
|
||||
avatar8,
|
||||
avatar9,
|
||||
avatar10,
|
||||
],
|
||||
details: {
|
||||
name: 'Restricted User',
|
||||
permissions: [
|
||||
{
|
||||
name: 'User Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Content Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Disputes Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Database Management',
|
||||
read: true,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const isRoleDialogVisible = ref(false)
|
||||
const roleDetail = ref()
|
||||
const isAddRoleDialogVisible = ref(false)
|
||||
|
||||
const editPermission = value => {
|
||||
isRoleDialogVisible.value = true
|
||||
roleDetail.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- 👉 Roles -->
|
||||
<VCol
|
||||
v-for="item in roles"
|
||||
:key="item.role"
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center">
|
||||
<span>Total {{ item.users.length }} users</span>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="v-avatar-group">
|
||||
<template
|
||||
v-for="(user, index) in item.users"
|
||||
:key="user"
|
||||
>
|
||||
<VAvatar
|
||||
v-if="item.users.length > 4 && item.users.length !== 4 && index < 3"
|
||||
size="40"
|
||||
:image="user"
|
||||
/>
|
||||
|
||||
<VAvatar
|
||||
v-if="item.users.length === 4"
|
||||
size="40"
|
||||
:image="user"
|
||||
/>
|
||||
</template>
|
||||
<VAvatar
|
||||
v-if="item.users.length > 4"
|
||||
:color="$vuetify.theme.name === 'dark' ? '#3F3B59' : '#F0EFF0'"
|
||||
>
|
||||
<span class="text-high-emphasis">
|
||||
+{{ item.users.length - 3 }}
|
||||
</span>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<h5 class="text-h5 mb-1">
|
||||
{{ item.role }}
|
||||
</h5>
|
||||
<div class="d-flex align-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
@click="editPermission(item.details)"
|
||||
>
|
||||
Edit Role
|
||||
</a>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<IconBtn
|
||||
color="secondary"
|
||||
class="mt-n2"
|
||||
>
|
||||
<VIcon icon="ri-file-copy-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Add New Role -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
>
|
||||
<VCard
|
||||
class="h-100"
|
||||
:ripple="false"
|
||||
>
|
||||
<VRow
|
||||
no-gutters
|
||||
class="h-100"
|
||||
>
|
||||
<VCol
|
||||
cols="5"
|
||||
class="d-flex flex-column justify-end align-center mt-5"
|
||||
>
|
||||
<img
|
||||
width="65"
|
||||
:src="poseM"
|
||||
>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="7">
|
||||
<VCardText class="d-flex flex-column align-end justify-end gap-4">
|
||||
<VBtn
|
||||
size="small"
|
||||
@click="isAddRoleDialogVisible = true"
|
||||
>
|
||||
Add Role
|
||||
</VBtn>
|
||||
<span class="text-end">Add role, if it doesn't exist.</span>
|
||||
</VCardText>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
<AddEditRoleDialog v-model:is-dialog-visible="isAddRoleDialogVisible" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<AddEditRoleDialog
|
||||
v-model:is-dialog-visible="isRoleDialogVisible"
|
||||
v-model:role-permissions="roleDetail"
|
||||
/>
|
||||
</template>
|
359
resources/js/views/apps/roles/UserList.vue
Normal file
359
resources/js/views/apps/roles/UserList.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<script setup>
|
||||
const searchQuery = ref('')
|
||||
const selectedRole = ref()
|
||||
const selectedPlan = ref()
|
||||
const selectedStatus = ref()
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'User',
|
||||
key: 'user',
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: 'Role',
|
||||
key: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Plan',
|
||||
key: 'plan',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
data: usersData,
|
||||
execute: fetchUsers,
|
||||
} = await useApi(createUrl('/apps/users', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
plan: selectedPlan,
|
||||
role: selectedRole,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const users = computed(() => usersData.value.users)
|
||||
const totalUsers = computed(() => usersData.value.totalUsers)
|
||||
|
||||
// 👉 search filters
|
||||
const roles = [
|
||||
{
|
||||
title: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
title: 'Author',
|
||||
value: 'author',
|
||||
},
|
||||
{
|
||||
title: 'Editor',
|
||||
value: 'editor',
|
||||
},
|
||||
{
|
||||
title: 'Maintainer',
|
||||
value: 'maintainer',
|
||||
},
|
||||
{
|
||||
title: 'Subscriber',
|
||||
value: 'subscriber',
|
||||
},
|
||||
]
|
||||
|
||||
const resolveUserRoleVariant = role => {
|
||||
const roleLowerCase = role.toLowerCase()
|
||||
if (roleLowerCase === 'subscriber')
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
if (roleLowerCase === 'author')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'ri-computer-line',
|
||||
}
|
||||
if (roleLowerCase === 'maintainer')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'ri-pie-chart-line',
|
||||
}
|
||||
if (roleLowerCase === 'editor')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'ri-edit-box-line',
|
||||
}
|
||||
if (roleLowerCase === 'admin')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-vip-crown-line',
|
||||
}
|
||||
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveUserStatusVariant = stat => {
|
||||
const statLowerCase = stat.toLowerCase()
|
||||
if (statLowerCase === 'pending')
|
||||
return 'warning'
|
||||
if (statLowerCase === 'active')
|
||||
return 'success'
|
||||
if (statLowerCase === 'inactive')
|
||||
return 'secondary'
|
||||
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
const deleteUser = async id => {
|
||||
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
|
||||
|
||||
// refetch User
|
||||
|
||||
// TODO: Make this async
|
||||
fetchUsers()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<VCard>
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<!-- 👉 Export button -->
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prepend-icon="ri-share-box-line"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="app-user-search-filter d-flex flex-wrap gap-4">
|
||||
<!-- 👉 Search -->
|
||||
|
||||
<div style="inline-size: 15.625rem;">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search User"
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Add user button -->
|
||||
<div style="inline-size: 10rem;">
|
||||
<VSelect
|
||||
v-model="selectedRole"
|
||||
placeholder="Select Role"
|
||||
:items="roles"
|
||||
density="compact"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- SECTION datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="[
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 20, title: '20' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
|
||||
]"
|
||||
:items="users"
|
||||
item-value="id"
|
||||
:items-length="totalUsers"
|
||||
:headers="headers"
|
||||
show-select
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- User -->
|
||||
<template #item.user="{ item }">
|
||||
<div class="d-flex">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:variant="!item.avatar ? 'tonal' : undefined"
|
||||
:color="!item.avatar ? resolveUserRoleVariant(item.role).color : undefined"
|
||||
class="me-3"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.fullName) }}</span>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
|
||||
class="text-h6"
|
||||
>
|
||||
{{ item.fullName }}
|
||||
</RouterLink>
|
||||
<span class="text-sm">@{{ item.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Role -->
|
||||
<template #item.role="{ item }">
|
||||
<div class="d-flex gap-4">
|
||||
<VIcon
|
||||
size="22"
|
||||
:icon="resolveUserRoleVariant(item.role).icon"
|
||||
:color="resolveUserRoleVariant(item.role).color"
|
||||
/>
|
||||
<h6 class="text-h6 text-capitalize font-weight-regular">
|
||||
{{ item.role }}
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Plan -->
|
||||
<template #item.plan="{ item }">
|
||||
<h6 class="text-h6 font-weight-regular text-capitalize">
|
||||
{{ item.currentPlan }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="resolveUserStatusVariant(item.status)"
|
||||
size="small"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteUser(item.id)"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
|
||||
>
|
||||
<VIcon icon="ri-eye-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem link>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="ri-edit-box-line"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Edit</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem link>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="ri-download-line"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Download</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalUsers) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalUsers / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalUsers / itemsPerPage) ? page = Math.ceil(totalUsers / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
<!-- SECTION -->
|
||||
</VCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
212
resources/js/views/apps/user/list/AddNewUserDrawer.vue
Normal file
212
resources/js/views/apps/user/list/AddNewUserDrawer.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'userData',
|
||||
])
|
||||
|
||||
const isFormValid = ref(false)
|
||||
const refForm = ref()
|
||||
const fullName = ref('')
|
||||
const userName = ref('')
|
||||
const email = ref('')
|
||||
const company = ref('')
|
||||
const country = ref()
|
||||
const contact = ref('')
|
||||
const role = ref()
|
||||
const plan = ref()
|
||||
const status = ref()
|
||||
|
||||
// 👉 drawer close
|
||||
const closeNavigationDrawer = () => {
|
||||
emit('update:isDrawerOpen', false)
|
||||
nextTick(() => {
|
||||
refForm.value?.reset()
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
refForm.value?.validate().then(({ valid }) => {
|
||||
if (valid) {
|
||||
emit('userData', {
|
||||
id: 0,
|
||||
fullName: fullName.value,
|
||||
company: company.value,
|
||||
role: role.value,
|
||||
username: userName.value,
|
||||
country: country.value,
|
||||
contact: contact.value,
|
||||
email: email.value,
|
||||
currentPlan: plan.value,
|
||||
status: status.value,
|
||||
avatar: '',
|
||||
})
|
||||
emit('update:isDrawerOpen', false)
|
||||
nextTick(() => {
|
||||
refForm.value?.reset()
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
:width="400"
|
||||
location="end"
|
||||
class="scrollable-content"
|
||||
:model-value="props.isDrawerOpen"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Title -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Add User"
|
||||
@cancel="closeNavigationDrawer"
|
||||
/>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<!-- 👉 Form -->
|
||||
<VForm
|
||||
ref="refForm"
|
||||
v-model="isFormValid"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<!-- 👉 Full name -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="fullName"
|
||||
:rules="[requiredValidator]"
|
||||
label="Full Name"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Username -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="userName"
|
||||
:rules="[requiredValidator]"
|
||||
label="Username"
|
||||
placeholder="Johndoe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Email -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="email"
|
||||
:rules="[requiredValidator, emailValidator]"
|
||||
label="Email"
|
||||
placeholder="johndoe@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 company -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="company"
|
||||
:rules="[requiredValidator]"
|
||||
label="Company"
|
||||
placeholder="Themeselection"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Country -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="country"
|
||||
label="Select Country"
|
||||
placeholder="Select Country"
|
||||
:rules="[requiredValidator]"
|
||||
:items="['USA', 'UK', 'India', 'Australia']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Contact -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="contact"
|
||||
type="number"
|
||||
:rules="[requiredValidator]"
|
||||
label="Contact"
|
||||
placeholder="+1-541-754-3010"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Role -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="role"
|
||||
label="Select Role"
|
||||
placeholder="Select Role"
|
||||
:rules="[requiredValidator]"
|
||||
:items="['Admin', 'Author', 'Editor', 'Maintainer', 'Subscriber']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Plan -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="plan"
|
||||
label="Select Plan"
|
||||
placeholder="Select Plan"
|
||||
:rules="[requiredValidator]"
|
||||
:items="['Basic', 'Company', 'Enterprise', 'Team']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Status -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="status"
|
||||
label="Select Status"
|
||||
placeholder="Select Status"
|
||||
:rules="[requiredValidator]"
|
||||
:items="[{ title: 'Active', value: 'active' }, { title: 'Inactive', value: 'inactive' }, { title: 'Pending', value: 'pending' }]"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Submit and Cancel -->
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-4"
|
||||
>
|
||||
Submit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
type="reset"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
@click="closeNavigationDrawer"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
1
resources/js/views/apps/user/types.js
Normal file
1
resources/js/views/apps/user/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
373
resources/js/views/apps/user/view/UserBioPanel.vue
Normal file
373
resources/js/views/apps/user/view/UserBioPanel.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
userData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const standardPlan = {
|
||||
plan: 'Standard',
|
||||
price: 99,
|
||||
benefits: [
|
||||
'10 Users',
|
||||
'Up to 10GB storage',
|
||||
'Basic Support',
|
||||
],
|
||||
}
|
||||
|
||||
const isUserInfoEditDialogVisible = ref(false)
|
||||
const isUpgradePlanDialogVisible = ref(false)
|
||||
|
||||
const resolveUserStatusVariant = stat => {
|
||||
if (stat === 'pending')
|
||||
return 'warning'
|
||||
if (stat === 'active')
|
||||
return 'success'
|
||||
if (stat === 'inactive')
|
||||
return 'secondary'
|
||||
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
const resolveUserRoleVariant = role => {
|
||||
if (role === 'subscriber')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
if (role === 'author')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'ri-settings-2-line',
|
||||
}
|
||||
if (role === 'maintainer')
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'ri-database-2-line',
|
||||
}
|
||||
if (role === 'editor')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'ri-pencil-line',
|
||||
}
|
||||
if (role === 'admin')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'ri-server-line',
|
||||
}
|
||||
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- SECTION User Details -->
|
||||
<VCol cols="12">
|
||||
<VCard v-if="props.userData">
|
||||
<VCardText class="text-center pt-12 pb-6">
|
||||
<!-- 👉 Avatar -->
|
||||
<VAvatar
|
||||
rounded
|
||||
:size="120"
|
||||
:color="!props.userData.avatar ? 'primary' : undefined"
|
||||
:variant="!props.userData.avatar ? 'tonal' : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="props.userData.avatar"
|
||||
:src="props.userData.avatar"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-5xl font-weight-medium"
|
||||
>
|
||||
{{ avatarText(props.userData.fullName) }}
|
||||
</span>
|
||||
</VAvatar>
|
||||
|
||||
<!-- 👉 User fullName -->
|
||||
<h5 class="text-h5 mt-4">
|
||||
{{ props.userData.fullName }}
|
||||
</h5>
|
||||
|
||||
<!-- 👉 Role chip -->
|
||||
<VChip
|
||||
:color="resolveUserRoleVariant(props.userData.role).color"
|
||||
size="small"
|
||||
class="text-capitalize mt-4"
|
||||
>
|
||||
{{ props.userData.role }}
|
||||
</VChip>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex justify-center flex-wrap gap-6 pb-6">
|
||||
<!-- 👉 Done task -->
|
||||
<div class="d-flex align-center me-8">
|
||||
<VAvatar
|
||||
:size="40"
|
||||
rounded
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
icon="ri-check-line"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<div>
|
||||
<h6 class="text-h5">
|
||||
{{ kFormatter(props.userData.taskDone) }}
|
||||
</h6>
|
||||
<span>Task Done</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Done Project -->
|
||||
<div class="d-flex align-center me-4">
|
||||
<VAvatar
|
||||
:size="44"
|
||||
rounded
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="me-3"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
icon="ri-briefcase-4-line"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
{{ kFormatter(props.userData.projectDone) }}
|
||||
</h6>
|
||||
<span>Project Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Details -->
|
||||
<VCardText class="pb-6">
|
||||
<h5 class="text-h5">
|
||||
Details
|
||||
</h5>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<!-- 👉 User Details list -->
|
||||
<VList class="card-list">
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">Username:</span>
|
||||
<span class="text-body-1">
|
||||
@{{ props.userData.username }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Billing Email:
|
||||
</span>
|
||||
<span class="text-body-1">{{ props.userData.email }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Status:
|
||||
</span>
|
||||
<VChip
|
||||
size="small"
|
||||
:color="resolveUserStatusVariant(props.userData.status)"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ props.userData.status }}
|
||||
</VChip>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">Role: </span>
|
||||
<span class="text-capitalize text-body-1">{{ props.userData.role }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Tax ID:
|
||||
</span>
|
||||
<span class="text-body-1">
|
||||
{{ props.userData.taxId }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Contact:
|
||||
</span>
|
||||
<span class="text-body-1">{{ props.userData.contact }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Language:
|
||||
</span>
|
||||
<span class="text-body-1">{{ props.userData.language }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">
|
||||
Country:
|
||||
</span>
|
||||
<span class="text-body-1">{{ props.userData.country }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Edit and Suspend button -->
|
||||
<VCardText class="d-flex justify-center">
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
class="me-4"
|
||||
@click="isUserInfoEditDialogVisible = true"
|
||||
>
|
||||
Edit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Suspend
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION Current Plan -->
|
||||
<VCol cols="12">
|
||||
<VCard
|
||||
flat
|
||||
class="current-plan"
|
||||
>
|
||||
<VCardText class="d-flex">
|
||||
<!-- 👉 Standard Chip -->
|
||||
<VChip
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Standard
|
||||
</VChip>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- 👉 Current Price -->
|
||||
<div class="d-flex align-center">
|
||||
<sup class="text-primary text-lg font-weight-medium">$</sup>
|
||||
<h1 class="text-h1 text-primary">
|
||||
99
|
||||
</h1>
|
||||
<sub class="mt-3"><h6 class="text-h6 font-weight-regular">month</h6></sub>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<!-- 👉 Price Benefits -->
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="benefit in standardPlan.benefits"
|
||||
:key="benefit"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon
|
||||
size="10"
|
||||
color="medium-emphasis"
|
||||
class="me-2"
|
||||
icon="ri-circle-fill"
|
||||
/>
|
||||
<div class="text-medium-emphasis">
|
||||
{{ benefit }}
|
||||
</div>
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<!-- 👉 Days -->
|
||||
<div class="my-6">
|
||||
<div class="d-flex mt-3 mb-2">
|
||||
<h6 class="text-h6 font-weight-medium">
|
||||
Days
|
||||
</h6>
|
||||
<VSpacer />
|
||||
<h6 class="text-h6 font-weight-medium">
|
||||
26 of 30 Days
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Progress -->
|
||||
<VProgressLinear
|
||||
rounded
|
||||
:model-value="86"
|
||||
height="8"
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<p class="text-sm mt-1">
|
||||
4 days remaining
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Upgrade Plan -->
|
||||
<VBtn
|
||||
block
|
||||
@click="isUpgradePlanDialogVisible = true"
|
||||
>
|
||||
Upgrade Plan
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<!-- !SECTION -->
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Edit user info dialog -->
|
||||
<UserInfoEditDialog
|
||||
v-model:isDialogVisible="isUserInfoEditDialogVisible"
|
||||
:user-data="props.userData"
|
||||
/>
|
||||
|
||||
<!-- 👉 Upgrade plan dialog -->
|
||||
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: .5rem;
|
||||
}
|
||||
|
||||
.current-plan {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize !important;
|
||||
}
|
||||
</style>
|
331
resources/js/views/apps/user/view/UserInvoiceTable.vue
Normal file
331
resources/js/views/apps/user/view/UserInvoiceTable.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<script setup>
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref()
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 👉 headers
|
||||
const headers = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: 'Trending',
|
||||
key: 'trending',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Total',
|
||||
key: 'total',
|
||||
},
|
||||
{
|
||||
title: 'Issued Date',
|
||||
key: 'date',
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
data: invoiceData,
|
||||
execute: fetchInvoices,
|
||||
} = await useApi(createUrl('/apps/invoice', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const invoices = computed(() => invoiceData.value?.invoices)
|
||||
const totalInvoices = computed(() => invoiceData.value?.totalInvoices)
|
||||
|
||||
// 👉 Invoice balance variant resolver
|
||||
const resolveInvoiceBalanceVariant = (balance, total) => {
|
||||
if (balance === total)
|
||||
return {
|
||||
status: 'Unpaid',
|
||||
chip: { color: 'error' },
|
||||
}
|
||||
if (balance === 0)
|
||||
return {
|
||||
status: 'Paid',
|
||||
chip: { color: 'success' },
|
||||
}
|
||||
|
||||
return {
|
||||
status: balance,
|
||||
chip: { variant: 'text' },
|
||||
}
|
||||
}
|
||||
|
||||
const resolveInvoiceStatusVariantAndIcon = status => {
|
||||
if (status === 'Partial Payment')
|
||||
return {
|
||||
variant: 'warning',
|
||||
icon: 'ri-line-chart-line',
|
||||
}
|
||||
if (status === 'Paid')
|
||||
return {
|
||||
variant: 'success',
|
||||
icon: 'ri-check-line',
|
||||
}
|
||||
if (status === 'Downloaded')
|
||||
return {
|
||||
variant: 'info',
|
||||
icon: 'ri-arrow-down-line',
|
||||
}
|
||||
if (status === 'Draft')
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'ri-save-line',
|
||||
}
|
||||
if (status === 'Sent')
|
||||
return {
|
||||
variant: 'primary',
|
||||
icon: 'ri-mail-line',
|
||||
}
|
||||
if (status === 'Past Due')
|
||||
return {
|
||||
variant: 'error',
|
||||
icon: 'ri-error-warning-line',
|
||||
}
|
||||
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'ri-close-line',
|
||||
}
|
||||
}
|
||||
|
||||
const computedMoreList = computed(() => {
|
||||
return paramId => [
|
||||
{
|
||||
title: 'Download',
|
||||
value: 'download',
|
||||
prependIcon: 'ri-download-line',
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
value: 'edit',
|
||||
prependIcon: 'ri-pencil-line',
|
||||
to: {
|
||||
name: 'apps-invoice-edit-id',
|
||||
params: { id: paramId },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
prependIcon: 'ri-stack-line',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const deleteInvoice = async id => {
|
||||
await $api(`/apps/invoice/${ id }`, { method: 'DELETE' })
|
||||
fetchInvoices()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="invoices">
|
||||
<VCard
|
||||
id="invoice-list"
|
||||
title=" Invoice List"
|
||||
>
|
||||
<template #append>
|
||||
<!-- 👉 Export invoice -->
|
||||
<VBtn>
|
||||
Export
|
||||
<VIcon
|
||||
end
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-line"
|
||||
/>
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- SECTION Datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:loading="isLoading"
|
||||
:items-length="totalInvoices"
|
||||
:headers="headers"
|
||||
:items="invoices"
|
||||
item-value="id"
|
||||
class="text-no-wrap text-sm rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- Trending Header -->
|
||||
<template #header.trending>
|
||||
<VIcon
|
||||
size="22"
|
||||
icon="ri-arrow-up-line"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- id -->
|
||||
<template #item.id="{ item }">
|
||||
<RouterLink :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
|
||||
#{{ item.id }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- trending -->
|
||||
<template #item.trending="{ item }">
|
||||
<VTooltip>
|
||||
<template #activator="{ props }">
|
||||
<VAvatar
|
||||
:size="28"
|
||||
v-bind="props"
|
||||
:color="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon
|
||||
:size="16"
|
||||
:icon="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).icon"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<p class="mb-0">
|
||||
{{ item.invoiceStatus }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
Balance: {{ item.balance }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
Due date: {{ item.dueDate }}
|
||||
</p>
|
||||
</VTooltip>
|
||||
</template>
|
||||
|
||||
<!-- Total -->
|
||||
<template #item.total="{ item }">
|
||||
${{ item.total }}
|
||||
</template>
|
||||
|
||||
<!-- issued Date -->
|
||||
<template #item.date="{ item }">
|
||||
{{ item.issuedDate }}
|
||||
</template>
|
||||
|
||||
<!-- Balance -->
|
||||
<template #item.balance="{ item }">
|
||||
<VChip
|
||||
v-if="typeof ((resolveInvoiceBalanceVariant(item.balance, item.total)).status) === 'string'"
|
||||
:color="resolveInvoiceBalanceVariant(item.balance, item.total).chip.color"
|
||||
>
|
||||
{{ (resolveInvoiceBalanceVariant(item.balance, item.total)).status }}
|
||||
</VChip>
|
||||
<span
|
||||
v-else
|
||||
class="text-sm text-high-emphasis"
|
||||
>
|
||||
{{ Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status) > 0 ? `$${(resolveInvoiceBalanceVariant(item.balance, item.total)).status}` : `-$${Math.abs(Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status))}` }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteInvoice(item.id)"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }"
|
||||
>
|
||||
<VIcon icon="ri-eye-line" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn
|
||||
size="small"
|
||||
:menu-list="computedMoreList(item.id)"
|
||||
item-props
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalInvoices) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalInvoices / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalInvoices / itemsPerPage) ? page = Math.ceil(totalInvoices / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
<!-- !SECTION -->
|
||||
</VCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#invoice-list {
|
||||
.invoice-list-actions {
|
||||
inline-size: 8rem;
|
||||
}
|
||||
|
||||
.invoice-list-search {
|
||||
inline-size: 12rem;
|
||||
}
|
||||
}
|
||||
</style>
|
403
resources/js/views/apps/user/view/UserTabBillingsPlans.vue
Normal file
403
resources/js/views/apps/user/view/UserTabBillingsPlans.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<script setup>
|
||||
import americanExpress from '@images/icons/payments/american-express.png'
|
||||
import mastercard from '@images/icons/payments/mastercard.png'
|
||||
import visa from '@images/icons/payments/visa.png'
|
||||
|
||||
const isUpgradePlanDialogVisible = ref(false)
|
||||
const currentCardDetails = ref()
|
||||
const isCardEditDialogVisible = ref(false)
|
||||
const isCardAddDialogVisible = ref(false)
|
||||
const isEditAddressDialogVisible = ref(false)
|
||||
|
||||
const openEditCardDialog = cardDetails => {
|
||||
currentCardDetails.value = cardDetails
|
||||
isCardEditDialogVisible.value = true
|
||||
}
|
||||
|
||||
const creditCards = [
|
||||
{
|
||||
name: 'Tom McBride',
|
||||
number: '4851234567899865',
|
||||
expiry: '12/24',
|
||||
isPrimary: true,
|
||||
type: 'mastercard',
|
||||
cvv: '123',
|
||||
image: mastercard,
|
||||
},
|
||||
{
|
||||
name: 'Mildred Wagner',
|
||||
number: '5531234567895678',
|
||||
expiry: '02/24',
|
||||
isPrimary: false,
|
||||
type: 'visa',
|
||||
cvv: '456',
|
||||
image: visa,
|
||||
},
|
||||
{
|
||||
name: 'Lester Jennings',
|
||||
number: '5531234567890002',
|
||||
expiry: '08/20',
|
||||
isPrimary: false,
|
||||
type: 'visa',
|
||||
cvv: '456',
|
||||
image: americanExpress,
|
||||
},
|
||||
]
|
||||
|
||||
const currentBillingAddress = {
|
||||
companyName: 'ThemeSelection',
|
||||
billingEmail: 'gertrude@gmail.com',
|
||||
taxID: 'TAX-875623',
|
||||
vatNumber: 'SDF754K77',
|
||||
address: '100 Water Plant Avenue, Building 1303 Wake Island',
|
||||
contact: '+1(609) 933-44-22',
|
||||
country: 'USA',
|
||||
state: 'Queensland',
|
||||
zipCode: 403114,
|
||||
}
|
||||
|
||||
const editBillingData = {
|
||||
firstName: 'Gertrude',
|
||||
lastName: 'Jennings',
|
||||
selectedCountry: 'USA',
|
||||
addressLine1: '100 Water Plant Avenue',
|
||||
addressLine2: 'Building 1303 Wake Island',
|
||||
landmark: 'Near Wake Island',
|
||||
contact: '+1(609) 933-44-22',
|
||||
country: 'USA',
|
||||
state: 'Queensland',
|
||||
zipCode: 403114,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- 👉 Current Plan -->
|
||||
<VCol cols="12">
|
||||
<VCard title="Current Plan">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<h6 class="text-h6 mb-1">
|
||||
Your Current Plan is Basic
|
||||
</h6>
|
||||
<p>A simple start for everyone</p>
|
||||
|
||||
<h6 class="text-h6 mb-1">
|
||||
Active until Dec 09, 2021
|
||||
</h6>
|
||||
<p>We will send you a notification upon Subscription expiration</p>
|
||||
|
||||
<h6 class="text-h6 mb-1">
|
||||
<span class="me-3">$199 Per Month</span>
|
||||
<VChip
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Popular
|
||||
</VChip>
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
Standard plan for small to medium businesses
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<!-- 👉 Alert -->
|
||||
<VAlert
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
icon="ri-alert-line"
|
||||
closable
|
||||
>
|
||||
<VAlertTitle>We need your attention!</VAlertTitle>
|
||||
<span>Your plan requires update</span>
|
||||
</VAlert>
|
||||
|
||||
<!-- 👉 Progress -->
|
||||
<div class="d-flex justify-space-between font-weight-bold mt-4 mb-1">
|
||||
<h6 class="text-h6">
|
||||
Days
|
||||
</h6>
|
||||
<h6 class="text-h6">
|
||||
26 of 30 Days
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<VProgressLinear
|
||||
rounded
|
||||
color="primary"
|
||||
:height="10"
|
||||
:model-value="75"
|
||||
/>
|
||||
<p class="text-sm mt-1">
|
||||
Your plan requires update
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<VBtn @click="isUpgradePlanDialogVisible = true">
|
||||
upgrade plan
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
>
|
||||
Cancel Subscription
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Payment Methods -->
|
||||
<VCol cols="12">
|
||||
<VCard title="Payment Methods">
|
||||
<template #append>
|
||||
<VBtn
|
||||
size="small"
|
||||
prepend-icon="ri-add-line"
|
||||
@click="isCardAddDialogVisible = !isCardAddDialogVisible"
|
||||
>
|
||||
Add Card
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<VCardText class="d-flex flex-column gap-y-4">
|
||||
<VCard
|
||||
v-for="card in creditCards"
|
||||
:key="card.name"
|
||||
border
|
||||
flat
|
||||
>
|
||||
<VCardText class="d-flex flex-sm-row flex-column">
|
||||
<div class="text-no-wrap">
|
||||
<VImg
|
||||
:src="card.image"
|
||||
max-width="90"
|
||||
width="auto"
|
||||
:height="25"
|
||||
/>
|
||||
<h6 class="text-h6 my-2">
|
||||
{{ card.name }}
|
||||
<VChip
|
||||
v-if="card.isPrimary"
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Primary
|
||||
</VChip>
|
||||
</h6>
|
||||
<span class="text-body-1">**** **** **** {{ card.number.substring(card.number.length - 4) }}</span>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="d-flex flex-column text-sm-end">
|
||||
<div class="order-sm-0 order-1">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
class="me-4"
|
||||
size="small"
|
||||
@click="openEditCardDialog(card)"
|
||||
>
|
||||
Edit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
Delete
|
||||
</VBtn>
|
||||
</div>
|
||||
<span class="text-body-2 my-4 order-sm-1 order-0">Card expires at {{ card.expiry }}</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Billing Address -->
|
||||
<VCard title="Billing Address">
|
||||
<template #append>
|
||||
<VBtn
|
||||
size="small"
|
||||
prepend-icon="ri-add-line"
|
||||
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
|
||||
>
|
||||
Edit Address
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<VTable class="billing-address-table">
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
Company Name:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.companyName }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
Billing Email:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.billingEmail }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
Tax ID:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.taxID }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
VAT Number:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.vatNumber }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="d-flex align-baseline">
|
||||
<h6 class="text-h6 text-no-wrap">
|
||||
Billing Address:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-0">
|
||||
{{ currentBillingAddress.address }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</VTable>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<VTable class="billing-address-table">
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
Contact:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.contact }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
Country:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.country }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap mb-2">
|
||||
State:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-2">
|
||||
{{ currentBillingAddress.state }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6 text-no-wrap">
|
||||
Zip Code:
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1 mb-0">
|
||||
{{ currentBillingAddress.zipCode }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</VTable>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Edit Card Dialog -->
|
||||
<CardAddEditDialog
|
||||
v-model:isDialogVisible="isCardEditDialogVisible"
|
||||
:card-details="currentCardDetails"
|
||||
/>
|
||||
|
||||
<!-- 👉 Add Card Dialog -->
|
||||
<CardAddEditDialog v-model:isDialogVisible="isCardAddDialogVisible" />
|
||||
|
||||
<!-- 👉 Edit Address dialog -->
|
||||
<AddEditAddressDialog
|
||||
v-model:isDialogVisible="isEditAddressDialogVisible"
|
||||
:billing-address="editBillingData"
|
||||
/>
|
||||
|
||||
<!-- 👉 Upgrade plan dialog -->
|
||||
<UserUpgradePlanDialog v-model:isDialogVisible="isUpgradePlanDialogVisible" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.billing-address-table {
|
||||
tr {
|
||||
td:first-child {
|
||||
inline-size: 148px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
189
resources/js/views/apps/user/view/UserTabConnections.vue
Normal file
189
resources/js/views/apps/user/view/UserTabConnections.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<script setup>
|
||||
import asana from '@images/icons/brands/asana.png'
|
||||
import behance from '@images/icons/brands/behance.png'
|
||||
import dribbble from '@images/icons/brands/dribbble.png'
|
||||
import facebook from '@images/icons/brands/facebook.png'
|
||||
import github from '@images/icons/brands/github.png'
|
||||
import google from '@images/icons/brands/google.png'
|
||||
import linkedin from '@images/icons/brands/linkedin.png'
|
||||
import mailchimp from '@images/icons/brands/mailchimp.png'
|
||||
import slack from '@images/icons/brands/slack.png'
|
||||
import twitter from '@images/icons/brands/twitter.png'
|
||||
|
||||
const connectedAccounts = ref([
|
||||
{
|
||||
img: google,
|
||||
title: 'Google',
|
||||
text: 'Calendar and contacts',
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
img: slack,
|
||||
title: 'Slack',
|
||||
text: 'Communication',
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
img: github,
|
||||
title: 'GitHub',
|
||||
text: 'Manage your Git repositories',
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
img: mailchimp,
|
||||
title: 'Mailchimp',
|
||||
text: 'Email marketing service',
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
img: asana,
|
||||
title: 'Asana',
|
||||
text: 'Communication',
|
||||
connected: false,
|
||||
},
|
||||
])
|
||||
|
||||
const socialAccounts = ref([
|
||||
{
|
||||
img: facebook,
|
||||
title: 'Facebook',
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
img: twitter,
|
||||
title: 'Twitter',
|
||||
link: 'https://twitter.com/theme_selection',
|
||||
username: '@Theme_Selection',
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
img: linkedin,
|
||||
title: 'LinkedIn',
|
||||
link: 'https://www.linkedin.com/company/themeselection',
|
||||
username: '@ThemeSelection',
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
img: dribbble,
|
||||
title: 'Dribbble',
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
img: behance,
|
||||
title: 'Behance',
|
||||
connected: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- 👉 connected accounts -->
|
||||
<VCol cols="12">
|
||||
<VCard
|
||||
title="Connected Accounts"
|
||||
subtitle="Display content from your connected accounts on your site"
|
||||
>
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="account in connectedAccounts"
|
||||
:key="account.title"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
start
|
||||
:size="36"
|
||||
:image="account.img"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ account.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-1">
|
||||
{{ account.text }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VSwitch
|
||||
v-model="account.connected"
|
||||
density="compact"
|
||||
class="me-1"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 social accounts -->
|
||||
<VCol cols="12">
|
||||
<VCard
|
||||
title="Social Accounts"
|
||||
subtitle="Display content from social accounts on your site"
|
||||
>
|
||||
<VCardText>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="(account) in socialAccounts"
|
||||
:key="account.title"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
start
|
||||
size="36"
|
||||
rounded="0"
|
||||
:image="account.img"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ account.title }}
|
||||
</VListItemTitle>
|
||||
|
||||
<VListItemSubtitle v-if="account.connected">
|
||||
<a
|
||||
:href="account.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-base text-primary"
|
||||
>
|
||||
{{ account.username }}
|
||||
</a>
|
||||
</VListItemSubtitle>
|
||||
|
||||
<VListItemSubtitle
|
||||
v-else
|
||||
class="text-body-1"
|
||||
>
|
||||
Not connected
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VBtn
|
||||
icon
|
||||
:color="account.connected ? 'error' : 'secondary'"
|
||||
variant="outlined"
|
||||
class="rounded"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
:icon="account.connected ? 'ri-delete-bin-7-line' : 'ri-link'"
|
||||
/>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 16px;
|
||||
}
|
||||
</style>
|
87
resources/js/views/apps/user/view/UserTabNotifications.vue
Normal file
87
resources/js/views/apps/user/view/UserTabNotifications.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
const notifications = ref([
|
||||
{
|
||||
type: 'New for you',
|
||||
email: true,
|
||||
browser: false,
|
||||
app: false,
|
||||
},
|
||||
{
|
||||
type: 'Account activity',
|
||||
email: false,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'A new browser used to sign in',
|
||||
email: true,
|
||||
browser: true,
|
||||
app: true,
|
||||
},
|
||||
{
|
||||
type: 'A new device is linked',
|
||||
email: false,
|
||||
browser: true,
|
||||
app: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="Notifications">
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
You will receive notification for the below selected items.
|
||||
</h6>
|
||||
</VCardText>
|
||||
<VTable class="text-no-wrap rounded-0 text-high-emphasis">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
TYPE
|
||||
</th>
|
||||
<th scope="col">
|
||||
EMAIL
|
||||
</th>
|
||||
<th scope="col">
|
||||
BROWSER
|
||||
</th>
|
||||
<th scope="col">
|
||||
APP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="notification in notifications"
|
||||
:key="notification.type"
|
||||
>
|
||||
<td>{{ notification.type }}</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.email" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.browser" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="notification.app" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<VBtn>Save changes</VBtn>
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
286
resources/js/views/apps/user/view/UserTabOverview.vue
Normal file
286
resources/js/views/apps/user/view/UserTabOverview.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<script setup>
|
||||
import { useTheme } from 'vuetify'
|
||||
import UserInvoiceTable from './UserInvoiceTable.vue'
|
||||
import pdf from '@images/icons/project-icons/pdf.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import figma from '@images/icons/project-icons/figma.png'
|
||||
import html5 from '@images/icons/project-icons/html5.png'
|
||||
import python from '@images/icons/project-icons/python.png'
|
||||
import react from '@images/icons/project-icons/react.png'
|
||||
import sketch from '@images/icons/project-icons/sketch.png'
|
||||
import vue from '@images/icons/project-icons/vue.png'
|
||||
import xamarin from '@images/icons/project-icons/xamarin.png'
|
||||
|
||||
const projectTableHeaders = [
|
||||
{
|
||||
title: 'PROJECT',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'TOTAL TASK',
|
||||
key: 'totalTask',
|
||||
},
|
||||
{
|
||||
title: 'PROGRESS',
|
||||
key: 'progress',
|
||||
},
|
||||
{
|
||||
title: 'HOURS',
|
||||
key: 'hours',
|
||||
},
|
||||
]
|
||||
|
||||
const { name } = useTheme()
|
||||
|
||||
const projects = [
|
||||
{
|
||||
logo: react,
|
||||
name: 'BGC eCommerce App',
|
||||
project: 'React Project',
|
||||
totalTask: '122/240',
|
||||
progress: 78,
|
||||
hours: '18:42',
|
||||
},
|
||||
{
|
||||
logo: figma,
|
||||
name: 'Falcon Logo Design',
|
||||
project: 'Figma Project',
|
||||
totalTask: '09/56',
|
||||
progress: 18,
|
||||
hours: '20:42',
|
||||
},
|
||||
{
|
||||
logo: vue,
|
||||
name: 'Dashboard Design',
|
||||
project: 'Vuejs Project',
|
||||
totalTask: '290/320',
|
||||
progress: 62,
|
||||
hours: '120:87',
|
||||
},
|
||||
{
|
||||
logo: xamarin,
|
||||
name: 'Foodista mobile app',
|
||||
project: 'Xamarin Project',
|
||||
totalTask: '290/320',
|
||||
progress: 8,
|
||||
hours: '120:87',
|
||||
},
|
||||
{
|
||||
logo: python,
|
||||
name: 'Dojo Email App',
|
||||
project: 'Python Project',
|
||||
totalTask: '120/186',
|
||||
progress: 49,
|
||||
hours: '230:10',
|
||||
},
|
||||
{
|
||||
logo: sketch,
|
||||
name: 'Blockchain Website',
|
||||
project: 'Sketch Project',
|
||||
totalTask: '99/109',
|
||||
progress: 92,
|
||||
hours: '342:41',
|
||||
},
|
||||
{
|
||||
logo: html5,
|
||||
name: 'Hoffman Website',
|
||||
project: 'HTML Project',
|
||||
totalTask: '98/110',
|
||||
progress: 88,
|
||||
hours: '12:45',
|
||||
},
|
||||
]
|
||||
|
||||
const resolveUserProgressVariant = progress => {
|
||||
if (progress <= 25)
|
||||
return 'error'
|
||||
if (progress > 25 && progress <= 50)
|
||||
return 'warning'
|
||||
if (progress > 50 && progress <= 75)
|
||||
return 'primary'
|
||||
if (progress > 75 && progress <= 100)
|
||||
return 'success'
|
||||
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="Project List">
|
||||
<template #append>
|
||||
<VTextField
|
||||
v-model="search"
|
||||
placeholder="Search Project"
|
||||
density="compact"
|
||||
style="inline-size: 10rem;"
|
||||
/>
|
||||
</template>
|
||||
<!-- 👉 User Project List Table -->
|
||||
|
||||
<!-- SECTION Datatable -->
|
||||
<VDataTable
|
||||
:search="search"
|
||||
:headers="projectTableHeaders"
|
||||
:items="projects"
|
||||
item-value="name"
|
||||
class="text-no-wrap rounded-0"
|
||||
>
|
||||
<!-- projects -->
|
||||
<template #item.name="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
:size="34"
|
||||
class="me-3"
|
||||
:image="item.logo"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-h6 mb-0">
|
||||
{{ item.name }}
|
||||
</h6>
|
||||
<p class="text-sm text-medium-emphasis mb-0">
|
||||
{{ item.project }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- total task -->
|
||||
<template #item.totalTask="{ item }">
|
||||
<div class="text-high-emphasis">
|
||||
{{ item.totalTask }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Progress -->
|
||||
<template #item.progress="{ item }">
|
||||
<div class="text-high-emphasis">
|
||||
{{ item.progress }}%
|
||||
</div>
|
||||
<VProgressLinear
|
||||
:height="6"
|
||||
:model-value="item.progress"
|
||||
rounded
|
||||
:color="resolveUserProgressVariant(item.progress)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- remove footer -->
|
||||
<!-- TODO refactor this after vuetify community gives answer -->
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
<!-- !SECTION -->
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Activity timeline -->
|
||||
<VCard title="User Activity Timeline">
|
||||
<VCardText>
|
||||
<VTimeline
|
||||
density="compact"
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
:line-inset="8"
|
||||
class="v-timeline-density-compact"
|
||||
>
|
||||
<VTimelineItem
|
||||
dot-color="error"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
|
||||
<span class="app-timeline-title">
|
||||
12 Invoices have been paid
|
||||
</span>
|
||||
<span class="app-timeline-meta">12 min ago</span>
|
||||
</div>
|
||||
|
||||
<p class="app-timeline-text mb-2">
|
||||
Invoices have been paid to the company
|
||||
</p>
|
||||
<div class="d-inline-flex align-center timeline-chip">
|
||||
<img
|
||||
:src="pdf"
|
||||
height="20"
|
||||
class="me-2"
|
||||
alt="img"
|
||||
>
|
||||
|
||||
<span class="app-timeline-text font-weight-medium">
|
||||
invoice.pdf
|
||||
</span>
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
|
||||
<span class="app-timeline-title">
|
||||
Client Meeting
|
||||
</span>
|
||||
<span class="app-timeline-meta">45 min ago</span>
|
||||
</div>
|
||||
|
||||
<p class="app-timeline-text mb-2">
|
||||
React Project meeting with john @10:15am
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-center mt-3">
|
||||
<VAvatar
|
||||
size="32"
|
||||
class="me-2"
|
||||
:image="avatar2"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm font-weight-medium mb-0">
|
||||
Lester McCarthy (Client)
|
||||
</p>
|
||||
<span class="text-sm">CEO of Kelly Group</span>
|
||||
</div>
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="info"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-2 mb-3">
|
||||
<span class="app-timeline-title">
|
||||
Create a new project for client
|
||||
</span>
|
||||
<span class="app-timeline-meta">2 day ago</span>
|
||||
</div>
|
||||
|
||||
<p class="app-timeline-text mb-2">
|
||||
6 team members in a project
|
||||
</p>
|
||||
|
||||
<div class="v-avatar-group">
|
||||
<VAvatar
|
||||
v-for="avatar in [avatar2, avatar3, avatar4, avatar5]"
|
||||
:key="avatar"
|
||||
:image="avatar"
|
||||
/>
|
||||
<VAvatar :color="name === 'light' ? '#F0EFF0' : '#3F3B59'">
|
||||
<span class="text-high-emphasis">+3</span>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<UserInvoiceTable />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
198
resources/js/views/apps/user/view/UserTabSecurity.vue
Normal file
198
resources/js/views/apps/user/view/UserTabSecurity.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script setup>
|
||||
import chrome from '@images/logos/chrome.png'
|
||||
|
||||
const isNewPasswordVisible = ref(false)
|
||||
const isConfirmPasswordVisible = ref(false)
|
||||
const smsVerificationNumber = ref('')
|
||||
const isTwoFactorDialogOpen = ref(false)
|
||||
|
||||
const recentDeviceHeader = [
|
||||
{
|
||||
title: 'BROWSER',
|
||||
key: 'browser',
|
||||
},
|
||||
{
|
||||
title: 'DEVICE',
|
||||
key: 'device',
|
||||
},
|
||||
{
|
||||
title: 'LOCATION',
|
||||
key: 'location',
|
||||
},
|
||||
{
|
||||
title: 'RECENT ACTIVITY',
|
||||
key: 'activity',
|
||||
},
|
||||
]
|
||||
|
||||
const recentDevices = [
|
||||
{
|
||||
browser: 'Chrome on Windows',
|
||||
logo: chrome,
|
||||
device: 'Dell XPS 15',
|
||||
location: 'United States',
|
||||
activity: '10, Jan 2020 20:07',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on Android',
|
||||
logo: chrome,
|
||||
device: 'Google Pixel 3a',
|
||||
location: 'Ghana',
|
||||
activity: '11, Jan 2020 10:16',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on macOS',
|
||||
logo: chrome,
|
||||
device: 'Apple iMac',
|
||||
location: 'Mayotte',
|
||||
activity: '11, Jan 2020 12:10',
|
||||
},
|
||||
{
|
||||
browser: 'Chrome on iPhone',
|
||||
logo: chrome,
|
||||
device: 'Apple iPhone XR',
|
||||
location: 'Mauritania',
|
||||
activity: '12, Jan 2020 8:29',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Change password -->
|
||||
<VCard title="Change Password">
|
||||
<VCardText>
|
||||
<VAlert
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
closable
|
||||
class="mb-6"
|
||||
>
|
||||
<VAlertTitle>Ensure that these requirements are met</VAlertTitle>
|
||||
<span>Minimum 8 characters long, uppercase & symbol</span>
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="New Password"
|
||||
placeholder="············"
|
||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Confirm Password"
|
||||
placeholder="············"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VBtn type="submit">
|
||||
Change Password
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Two step verification -->
|
||||
<VCard
|
||||
title="Two-step verification"
|
||||
subtitle="Keep your account secure with authentication step."
|
||||
>
|
||||
<VCardText>
|
||||
<div>
|
||||
<h4 class="font-weight-medium mb-1">
|
||||
SMS
|
||||
</h4>
|
||||
<VTextField
|
||||
:model-value="smsVerificationNumber"
|
||||
readonly
|
||||
placeholder="+1(968) 819-2547"
|
||||
density="compact"
|
||||
>
|
||||
<template #append>
|
||||
<IconBtn
|
||||
rounded
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
class="me-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-edit-box-line"
|
||||
@click="isTwoFactorDialogOpen = true"
|
||||
/>
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
rounded
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
<VIcon icon="ri-user-add-line" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
|
||||
<p class="mb-0 mt-4">
|
||||
Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in. <a
|
||||
href="javascript:void(0)"
|
||||
class="text-decoration-none"
|
||||
>Learn more</a>.
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 Recent devices -->
|
||||
<VCard title="Recent devices">
|
||||
<VDataTable
|
||||
:items="recentDevices"
|
||||
:headers="recentDeviceHeader"
|
||||
hide-default-footer
|
||||
class="text-no-wrap rounded-0"
|
||||
>
|
||||
<template #item.browser="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
:image="item.logo"
|
||||
:size="22"
|
||||
class="me-3"
|
||||
/>
|
||||
<h6 class="text-h6 font-weight-medium">
|
||||
{{ item.browser }}
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Enable One Time Password Dialog -->
|
||||
<TwoFactorAuthDialog
|
||||
v-model:isDialogVisible="isTwoFactorDialogOpen"
|
||||
:sms-code="smsVerificationNumber"
|
||||
/>
|
||||
</template>
|
Reference in New Issue
Block a user