first commit

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

View File

@@ -0,0 +1,308 @@
<script setup>
import { VideoPlayer } from '@videojs-player/vue'
import instructorPosterImage from '@images/pages/instructor-poster-image.png'
import 'video.js/dist/video-js.css'
const courseDetails = ref()
const { data, error } = await useApi('/apps/academy/course-details')
if (error.value)
console.log(error.value)
else if (data.value)
courseDetails.value = data.value
const panelStatus = ref(0)
</script>
<template>
<VRow>
<VCol
cols="12"
md="8"
>
<VCard>
<VCardItem
title="UI/UX Basic Fundamentals"
class="pb-6"
>
<template #subtitle>
<div class="text-body-1">
Prof. <span class="text-h6 d-inline-block">{{ courseDetails?.title }}</span>
</div>
</template>
<template #append>
<div class="d-flex gap-4 align-center">
<VChip
variant="tonal"
color="error"
size="small"
>
UI/UX
</VChip>
<VIcon
size="20"
class="cursor-pointer"
icon="ri-share-forward-line"
/>
<VIcon
size="20"
class="cursor-pointer"
icon="ri-bookmark-line"
/>
</div>
</template>
</VCardItem>
<VCardText>
<VCard
flat
border
>
<div class="px-2 pt-2">
<VideoPlayer
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
:poster="instructorPosterImage"
controls
plays-inline
:height="$vuetify.display.mdAndUp ? 440 : 250"
class="w-100 rounded"
/>
</div>
<VCardText>
<h5 class="text-h5 mb-4">
About this course
</h5>
<p class="text-body-1">
{{ courseDetails?.about }}
</p>
<VDivider class="my-6" />
<h5 class="text-h5 mb-4">
By the numbers
</h5>
<div class="d-flex gap-x-12 gap-y-5 flex-wrap">
<div>
<VList class="card-list text-medium-emphasis">
<VListItem>
<template #prepend>
<VIcon
icon="ri-check-line"
size="20"
/>
</template>
<VListItemTitle>Skill Level: {{ courseDetails?.skillLevel }}</VListItemTitle>
</VListItem>
<VListItem>
<template #prepend>
<VIcon
icon="ri-user-line"
size="20"
/>
</template>
<VListItemTitle>Students: {{ courseDetails?.totalStudents }}</VListItemTitle>
</VListItem>
<VListItem>
<template #prepend>
<VIcon
icon="ri-global-line"
size="20"
/>
</template>
<VListItemTitle>Languages: {{ courseDetails?.language }}</VListItemTitle>
</VListItem>
<VListItem>
<template #prepend>
<VIcon
icon="ri-closed-captioning-line"
size="20"
/>
</template>
<VListItemTitle>Captions: {{ courseDetails?.isCaptions }}</VListItemTitle>
</VListItem>
</VList>
</div>
<div>
<VList class="card-list text-medium-emphasis">
<VListItem>
<template #prepend>
<VIcon
icon="ri-pencil-line"
size="20"
/>
</template>
<VListItemTitle>Lectures: {{ courseDetails?.totalLectures }}</VListItemTitle>
</VListItem>
<VListItem>
<template #prepend>
<VIcon
icon="ri-time-line"
size="20"
/>
</template>
<VListItemTitle>Video: {{ courseDetails?.length }}</VListItemTitle>
</VListItem>
</VList>
</div>
</div>
<VDivider class="my-6" />
<h5 class="text-h5 mb-4">
Description
</h5>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="courseDetails?.description" />
<VDivider class="my-6" />
<h5 class="text-h5 mb-4">
Instructor
</h5>
<div class="d-flex align-center">
<VAvatar
:image="courseDetails?.instructorAvatar"
size="38"
class="me-3"
/>
<div>
<h6 class="text-h6 mb-1">
{{ courseDetails?.instructor }}
</h6>
<div class="text-body-1">
{{ courseDetails?.instructorPosition }}
</div>
</div>
</div>
</VCardText>
</VCard>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="4"
>
<div class="course-content">
<VExpansionPanels
v-model="panelStatus"
variant="accordion"
>
<VExpansionPanel
v-for="(section, index) in courseDetails?.content"
:key="index"
elevation="0"
collapse-icon="ri-arrow-down-s-line"
:expand-icon="$vuetify.locale.isRtl ? 'ri-arrow-left-s-line' : 'ri-arrow-right-s-line'"
:value="index"
>
<template #title>
<div>
<h5 class="text-h5">
{{ section.title }}
</h5>
<div class="text-body-1">
{{ section.status }} | {{ section.time }}
</div>
</div>
</template>
<template #text>
<VList class="card-list">
<VListItem
v-for="(topic, id) in section.topics"
:key="id"
class="py-4"
>
<template #prepend>
<VCheckbox
class="ps-3 me-3"
:model-value="topic.isCompleted"
/>
</template>
<VListItemTitle>
<h6 class="text-h6">
{{ topic.title }}
</h6>
</VListItemTitle>
<VListItemSubtitle>
<div class="text-body-2">
{{ topic.time }}
</div>
</VListItemSubtitle>
</VListItem>
</VList>
</template>
</VExpansionPanel>
</VExpansionPanels>
</div>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.course-content {
position: sticky;
inset-block: 4rem 0;
}
.card-list {
--v-card-list-gap: 16px;
}
</style>
<style lang="scss">
.course-content{
.v-expansion-panels{
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
.v-expansion-panel{
&--active{
.v-expansion-panel-title--active{
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
.v-expansion-panel-title__overlay{
opacity: var(--v-hover-opacity) !important;
}
}
}
.v-expansion-panel-title{
.v-expansion-panel-title__overlay{
background-color: rgba(var(--v-theme-on-surface));
opacity: var(--v-hover-opacity);
}
&:hover{
.v-expansion-panel-title__overlay{
opacity: var(--v-hover-opacity) !important;
}
}
&__icon{
.v-icon{
block-size: 1.5rem;
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
font-size: 1.5rem;
inline-size: 1.5rem;
}
}
}
.v-expansion-panel-text{
&__wrapper{
padding-block: 1rem;
padding-inline: 1.25rem;
}
}
}
}
}
.card-list {
.v-list-item__prepend{
.v-list-item__spacer{
inline-size: 8px !important;
}
}
}
</style>

View File

@@ -0,0 +1,356 @@
<script setup>
import AcademyAssignmentProgress from '@/views/apps/academy/AcademyAssignmentProgress.vue'
import AcademyCardInterestedTopics from '@/views/apps/academy/AcademyCardInterestedTopics.vue'
import AcademyCardPopularInstructors from '@/views/apps/academy/AcademyCardPopularInstructors.vue'
import AcademyCardTopCourses from '@/views/apps/academy/AcademyCardTopCourses.vue'
import AcademyCourseTable from '@/views/apps/academy/AcademyCourseTable.vue'
import AcademyUpcomingWebinar from '@/views/apps/academy/AcademyUpcomingWebinar.vue'
import customCheck from '@images/svg/check.svg'
import customLaptop from '@images/svg/laptop.svg'
import customLightbulb from '@images/svg/lightbulb.svg'
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
const topicsChartConfig = {
chart: {
height: 270,
type: 'bar',
toolbar: { show: false },
},
plotOptions: {
bar: {
horizontal: true,
barHeight: '70%',
distributed: true,
borderRadius: 7,
},
},
colors: [
'#8C57FF',
'#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 donutChartColors = {
donut: {
series1: '#22A95E',
series2: '#24B364',
series3: '#56CA00',
series4: '#53D28C',
series5: '#7EDDA9',
series6: '#A9E9C5',
},
}
const timeSpendingChartConfig = {
chart: {
height: 157,
width: 130,
parentHeightOffset: 0,
type: 'donut',
},
labels: [
'36h',
'56h',
'16h',
'32h',
'56h',
'16h',
],
colors: [
donutChartColors.donut.series1,
donutChartColors.donut.series2,
donutChartColors.donut.series3,
donutChartColors.donut.series4,
donutChartColors.donut.series5,
donutChartColors.donut.series6,
],
stroke: { width: 0 },
dataLabels: {
enabled: false,
formatter(val) {
return `${ Number.parseInt(val) }%`
},
},
legend: { show: false },
tooltip: { theme: false },
grid: { padding: { top: 0 } },
plotOptions: {
pie: {
donut: {
size: '75%',
labels: {
show: true,
value: {
fontSize: '1.125rem',
color: 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))',
fontWeight: 500,
offsetY: -15,
formatter(val) {
return `${ Number.parseInt(val) }%`
},
},
name: { offsetY: 20 },
total: {
show: true,
fontSize: '.7rem',
label: 'Total',
color: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
formatter() {
return '231h'
},
},
},
},
},
},
}
const timeSpendingChartSeries = [
23,
35,
10,
20,
35,
23,
]
</script>
<template>
<div>
<VRow class="py-5 match-height">
<!-- 👉 Welcome -->
<VCol
cols="12"
md="8"
sm="6"
:class="$vuetify.display.smAndUp ? 'border-e' : 'border-b'"
>
<div class="pe-3">
<h5 class="text-h5 text-high-emphasis mb-1">
Welcome back, Felecia 👋🏻
</h5>
<div
class="text-wrap text-medium-emphasis mb-4"
style="max-inline-size: 400px;"
>
Your progress this week is Awesome. let's keep it up
and get a lot of points reward!
</div>
<div class="d-flex justify-space-between flex-wrap gap-6 flex-column flex-md-row">
<div
v-for="{ title, value, icon, color } in [
{ title: 'Hours Spent', value: '34h', icon: customLaptop, color: 'primary' },
{ title: 'Test Results', value: '82%', icon: customLightbulb, color: 'info' },
{ title: 'Course Completed', value: '14', icon: customCheck, color: 'warning' },
]"
:key="title"
>
<div class="d-flex">
<VAvatar
variant="tonal"
:color="color"
rounded
size="54"
class="text-primary me-4"
>
<VIcon
:icon="icon"
size="38"
/>
</VAvatar>
<div>
<h6 class="text-h6 text-medium-emphasis">
{{ title }}
</h6>
<h4
class="text-h4 font-weight-medium"
:class="`text-${color}`"
>
{{ value }}
</h4>
</div>
</div>
</div>
</div>
</div>
</VCol>
<!-- 👉 Time Spending -->
<VCol
cols="12"
md="4"
sm="6"
>
<div class="d-flex justify-space-between align-center">
<div class="d-flex flex-column ps-3">
<h5 class="text-h5 mb-1 text-no-wrap">
Time Spending
</h5>
<div class="mb-6 text-body-1">
Weekly Report
</div>
<h4 class="text-h4 mb-2">
231<span class="text-medium-emphasis">h</span> 14<span class="text-medium-emphasis">m</span>
</h4>
<div>
<VChip
color="success"
density="comfortable"
>
+18.4%
</VChip>
</div>
</div>
<div>
<VueApexCharts
type="donut"
height="150"
width="150"
:options="timeSpendingChartConfig"
:series="timeSpendingChartSeries"
/>
</div>
</div>
</VCol>
</VRow>
<VRow class="match-height">
<VCol
cols="12"
md="8"
>
<!-- 👉 Topic You are Interested in -->
<AcademyCardInterestedTopics />
</VCol>
<!-- 👉 Popular Instructors -->
<VCol
cols="12"
md="4"
sm="6"
>
<AcademyCardPopularInstructors />
</VCol>
<!-- 👉 Academy Top Courses -->
<VCol
cols="12"
md="4"
sm="6"
>
<AcademyCardTopCourses />
</VCol>
<!-- 👉 Academy Upcoming Webinar -->
<VCol
cols="12"
md="4"
sm="6"
>
<AcademyUpcomingWebinar />
</VCol>
<!-- 👉 Academy Assignment Progress -->
<VCol
cols="12"
md="4"
sm="6"
>
<AcademyAssignmentProgress />
</VCol>
<!-- 👉 Academy Course Table -->
<VCol>
<AcademyCourseTable />
</VCol>
</VRow>
</div>
</template>
<style lang="scss">
@use "@core-scss/template/libs/apex-chart.scss";
.topicCard{
.v-badge.v-badge--dot{
.v-badge__badge{
border-radius: 6px;
block-size: 12px;
inline-size: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,229 @@
<script setup>
import { VideoPlayer } from '@videojs-player/vue'
import AcademyMyCourses from '@/views/apps/academy/AcademyMyCourses.vue'
import academyCourseIllustration1 from '@images/pages/academy-course-illustration1.png'
import academyCourseIllustration2 from '@images/pages/academy-course-illustration2.png'
import boyAcademyIllustration from '@images/pages/boy-academy-illustration.png'
import girlAcademyIllustration from '@images/pages/girl-academy-illustration.png'
import guitarCoursePoster from '@images/pages/guitar-course.png'
import singingCoursePoster from '@images/pages/singing-course.png'
const searchQuery = ref('')
</script>
<template>
<div>
<VCard class="mb-6">
<VCardText class="py-12 position-relative">
<div
class="d-flex flex-column gap-y-4 mx-auto"
:class="$vuetify.display.mdAndUp ? 'w-50' : 'w-100'"
>
<h4
class="text-h4 text-center text-wrap mx-auto"
:class="$vuetify.display.mdAndUp ? 'w-75' : 'w-100'"
>
Education, talents, and career
opportunities. <span class="text-primary"> All in one place.</span>
</h4>
<p class="text-center text-wrap text-body-1 mx-auto mb-0">
Grow your skill with the most reliable online courses and certifications in marketing, information technology, programming, and data science.
</p>
<div class="d-flex justify-center align-center gap-x-4">
<VTextField
v-model="searchQuery"
density="compact"
placeholder="Find your course"
style="max-inline-size: 400px;"
/>
<VBtn
color="primary"
icon="ri-search-line"
class="rounded"
/>
</div>
</div>
<img
:src="academyCourseIllustration1"
class="illustration1 d-none d-md-block"
height="180"
>
<img
:src="academyCourseIllustration2"
class="illustration2 d-none d-md-block"
height="100"
>
</VCardText>
</VCard>
<AcademyMyCourses :search-query="searchQuery" />
<div class="mb-6">
<VRow>
<VCol
cols="12"
md="6"
>
<VCard
flat
color="rgba(var(--v-theme-primary), 0.08)"
>
<VCardText>
<div class="d-flex flex-column-reverse flex-sm-row gap-4 justify-space-between">
<div class="text-center text-sm-start">
<h5 class="text-h5 text-primary mb-1">
Earn a Certificate
</h5>
<p class="text-body-1 text-high-emphasis mb-6">
Get the right professional certificate program for you.
</p>
<VBtn>View Programs</VBtn>
</div>
<div class="text-center">
<img :src="boyAcademyIllustration">
</div>
</div>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="6"
>
<VCard
flat
color="rgba(var(--v-theme-error), 0.08)"
>
<VCardText>
<div class="d-flex flex-column-reverse flex-sm-row gap-4 justify-space-between">
<div class="text-center text-sm-start">
<h5 class="text-h5 text-error mb-1">
Best Rated Courses
</h5>
<p class="text-body-1 text-high-emphasis text-wrap mb-6">
Enroll now in the most popular and best rated courses.
</p>
<VBtn color="error">
View Courses
</VBtn>
</div>
<div class="text-center">
<img :src="girlAcademyIllustration">
</div>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
<VCard>
<VCardText>
<VRow>
<VCol
cols="12"
md="4"
>
<div class="d-flex flex-column align-center gap-y-4 h-100 justify-center">
<VAvatar
variant="tonal"
size="52"
rounded
color="primary"
>
<VIcon
icon="ri-gift-line"
size="36"
/>
</VAvatar>
<h4 class="text-h4">
Today's Free Courses
</h4>
<p class="text-body-1 text-center mb-0">
We offers 284 Free Online courses from top tutors and companies to help you start or advance your career skills. Learn online for free and fast today!
</p>
<VBtn>Get Premium Courses</VBtn>
</div>
</VCol>
<VCol
cols="12"
md="4"
sm="6"
>
<VCard
flat
border
>
<div class="pa-2">
<VideoPlayer
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
:poster="singingCoursePoster"
controls
plays-inline
:height="$vuetify.display.mdAndUp ? 200 : 150"
class="w-100 rounded"
/>
</div>
<VCardText class="pt-3">
<h5 class="text-h5 mb-2">
Your First Singing Lesson
</h5>
<p class="text-body-1 mb-0">
In the same way as any other artistic domain, singing lends itself perfectly to self-teaching.
</p>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="4"
sm="6"
>
<VCard
flat
border
>
<div class="pa-2">
<VideoPlayer
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
:poster="guitarCoursePoster"
controls
plays-inline
:height="$vuetify.display.mdAndUp ? 200 : 150"
class="w-100 rounded"
/>
</div>
<VCardText class="pt-3">
<h5 class="text-h5 mb-2">
Guitar for Beginners
</h5>
<p class="text-body-1 mb-0">
The Fender Acoustic Guitar is best choice for beginners and professionals.
</p>
</VCardText>
</VCard>
</VCol>
</VRow>
</VCardText>
</VCard>
</div>
</template>
<style lang="scss">
@import 'video.js/dist/video-js.css';
.illustration1 {
position: absolute;
inset-block-end: 0;
inset-inline-end: 0;
}
.illustration2 {
position: absolute;
inset-block-start: 2rem;
inset-inline-start: 2.5rem;
}
</style>

View File

@@ -0,0 +1,175 @@
<script setup>
import FullCalendar from '@fullcalendar/vue3'
import {
blankEvent,
useCalendar,
} from '@/views/apps/calendar/useCalendar'
import { useCalendarStore } from '@/views/apps/calendar/useCalendarStore'
// Components
import CalendarEventHandler from '@/views/apps/calendar/CalendarEventHandler.vue'
// 👉 Store
const store = useCalendarStore()
// 👉 Event
const event = ref(structuredClone(blankEvent))
const isEventHandlerSidebarActive = ref(false)
watch(isEventHandlerSidebarActive, val => {
if (!val)
event.value = structuredClone(blankEvent)
})
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
// 👉 useCalendar
const { refCalendar, calendarOptions, addEvent, updateEvent, removeEvent, jumpToDate } = useCalendar(event, isEventHandlerSidebarActive, isLeftSidebarOpen)
// SECTION Sidebar
// 👉 Check all
const checkAll = computed({
/*GET: Return boolean `true` => if length of options matches length of selected filters => Length matches when all events are selected
SET: If value is `true` => then add all available options in selected filters => Select All
Else if => all filters are selected (by checking length of both array) => Empty Selected array => Deselect All
*/
get: () => store.selectedCalendars.length === store.availableCalendars.length,
set: val => {
if (val)
store.selectedCalendars = store.availableCalendars.map(i => i.label)
else if (store.selectedCalendars.length === store.availableCalendars.length)
store.selectedCalendars = []
},
})
// !SECTION
const calendarApi = ref(null)
</script>
<template>
<div>
<VCard>
<!-- `z-index: 0` Allows overlapping vertical nav on calendar -->
<VLayout style="z-index: 0;">
<!-- 👉 Navigation drawer -->
<VNavigationDrawer
v-model="isLeftSidebarOpen"
width="292"
absolute
touchless
location="start"
class="calendar-add-event-drawer"
:temporary="$vuetify.display.mdAndDown"
>
<div class="pa-5">
<VBtn
block
prepend-icon="ri-add-line"
@click="isEventHandlerSidebarActive = true"
>
Add event
</VBtn>
</div>
<VDivider />
<div class="d-flex align-center justify-center pa-2">
<AppDateTimePicker
v-model="calendarApi"
:config="{ inline: true }"
class="calendar-date-picker"
@update:model-value="jumpToDate($event)"
/>
</div>
<VDivider />
<div class="pa-5">
<h5 class="text-h5 mb-4">
Event Filters
</h5>
<div class="d-flex flex-column calendars-checkbox">
<VCheckbox
v-model="checkAll"
label="View all"
/>
<VCheckbox
v-for="calendar in store.availableCalendars"
:key="calendar.label"
v-model="store.selectedCalendars"
:value="calendar.label"
:color="calendar.color"
:label="calendar.label"
/>
</div>
</div>
</VNavigationDrawer>
<VMain>
<VCard flat>
<FullCalendar
ref="refCalendar"
:options="calendarOptions"
/>
</VCard>
</VMain>
</VLayout>
</VCard>
<CalendarEventHandler
v-model:isDrawerOpen="isEventHandlerSidebarActive"
:event="event"
@add-event="addEvent"
@update-event="updateEvent"
@remove-event="removeEvent"
/>
</div>
</template>
<style lang="scss">
@use "@core-scss/template/libs/full-calendar";
.calendars-checkbox {
.v-label {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
opacity: var(--v-high-emphasis-opacity);
}
}
.calendar-add-event-drawer {
&.v-navigation-drawer:not(.v-navigation-drawer--temporary) {
border-end-start-radius: 0.375rem;
border-start-start-radius: 0.375rem;
}
}
.calendar-date-picker {
display: none;
+.flatpickr-input {
+.flatpickr-calendar.inline {
border: none;
box-shadow: none;
.flatpickr-months {
border-block-end: none;
}
}
}
& ~ .flatpickr-calendar .flatpickr-weekdays {
margin-block: 0 4px;
}
}
</style>
<style lang="scss" scoped>
.v-layout {
overflow: visible !important;
.v-card {
overflow: visible;
}
}
</style>

View File

@@ -0,0 +1,429 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import {
useDisplay,
useTheme,
} from 'vuetify'
import { themes } from '@/plugins/vuetify/theme'
import ChatActiveChatUserProfileSidebarContent from '@/views/apps/chat/ChatActiveChatUserProfileSidebarContent.vue'
import ChatLeftSidebarContent from '@/views/apps/chat/ChatLeftSidebarContent.vue'
import ChatLog from '@/views/apps/chat/ChatLog.vue'
import ChatUserProfileSidebarContent from '@/views/apps/chat/ChatUserProfileSidebarContent.vue'
import { useChat } from '@/views/apps/chat/useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
// composables
const vuetifyDisplays = useDisplay()
const store = useChatStore()
const { isLeftSidebarOpen } = useResponsiveLeftSidebar(vuetifyDisplays.smAndDown)
const { resolveAvatarBadgeVariant } = useChat()
// Perfect scrollbar
const chatLogPS = ref()
const scrollToBottomInChatLog = () => {
const scrollEl = chatLogPS.value.$el || chatLogPS.value
scrollEl.scrollTop = scrollEl.scrollHeight
}
// Search query
const q = ref('')
watch(q, val => store.fetchChatsAndContacts(val), { immediate: true })
// Open Sidebar in smAndDown when "start conversation" is clicked
const startConversation = () => {
if (vuetifyDisplays.mdAndUp.value)
return
isLeftSidebarOpen.value = true
}
// Chat message
const msg = ref('')
const sendMessage = async () => {
if (!msg.value)
return
await store.sendMsg(msg.value)
// Reset message input
msg.value = ''
// Scroll to bottom
nextTick(() => {
scrollToBottomInChatLog()
})
}
const openChatOfContact = async userId => {
await store.getChat(userId)
// Reset message input
msg.value = ''
// Set unseenMsgs to 0
const contact = store.chatsContacts.find(c => c.id === userId)
if (contact)
contact.chat.unseenMsgs = 0
// if smAndDown => Close Chat & Contacts left sidebar
if (vuetifyDisplays.smAndDown.value)
isLeftSidebarOpen.value = false
// Scroll to bottom
nextTick(() => {
scrollToBottomInChatLog()
})
}
// User profile sidebar
const isUserProfileSidebarOpen = ref(false)
// Active chat user profile sidebar
const isActiveChatUserProfileSidebarOpen = ref(false)
// file input
const refInputEl = ref()
const moreList = [
{
title: 'View Contact',
value: 'View Contact',
},
{
title: 'Mute Notifications',
value: 'Mute Notifications',
},
{
title: 'Block Contact',
value: 'Block Contact',
},
{
title: 'Clear Chat',
value: 'Clear Chat',
},
{
title: 'Report',
value: 'Report',
},
]
const { name } = useTheme()
const chatContentContainerBg = computed(() => {
let color = 'transparent'
if (themes)
color = themes?.[name.value].colors?.['chat-bg']
return color
})
</script>
<template>
<VLayout class="chat-app-layout bg-surface">
<!-- 👉 user profile sidebar -->
<VNavigationDrawer
v-model="isUserProfileSidebarOpen"
temporary
touchless
absolute
class="user-profile-sidebar"
location="start"
width="370"
>
<ChatUserProfileSidebarContent @close="isUserProfileSidebarOpen = false" />
</VNavigationDrawer>
<!-- 👉 Active Chat sidebar -->
<VNavigationDrawer
v-model="isActiveChatUserProfileSidebarOpen"
width="374"
absolute
temporary
location="end"
touchless
class="active-chat-user-profile-sidebar"
>
<ChatActiveChatUserProfileSidebarContent @close="isActiveChatUserProfileSidebarOpen = false" />
</VNavigationDrawer>
<!-- 👉 Left sidebar -->
<VNavigationDrawer
v-model="isLeftSidebarOpen"
absolute
touchless
location="start"
width="370"
:temporary="$vuetify.display.smAndDown"
class="chat-list-sidebar"
:permanent="$vuetify.display.mdAndUp"
>
<ChatLeftSidebarContent
v-model:isDrawerOpen="isLeftSidebarOpen"
v-model:search="q"
@open-chat-of-contact="openChatOfContact"
@show-user-profile="isUserProfileSidebarOpen = true"
@close="isLeftSidebarOpen = false"
/>
</VNavigationDrawer>
<!-- 👉 Chat content -->
<VMain class="chat-content-container">
<!-- 👉 Right content: Active Chat -->
<div
v-if="store.activeChat"
class="d-flex flex-column h-100"
>
<!-- 👉 Active chat header -->
<div class="active-chat-header d-flex align-center text-medium-emphasis">
<!-- Sidebar toggler -->
<IconBtn
class="d-md-none me-4"
@click="isLeftSidebarOpen = true"
>
<VIcon icon="ri-menu-line" />
</IconBtn>
<!-- avatar -->
<div
class="d-flex align-center cursor-pointer"
@click="isActiveChatUserProfileSidebarOpen = true"
>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
bordered
class="me-4"
>
<VAvatar
size="40"
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
class="cursor-pointer"
>
<VImg
v-if="store.activeChat.contact.avatar"
:src="store.activeChat.contact.avatar"
:alt="store.activeChat.contact.fullName"
/>
<span v-else>{{ avatarText(store.activeChat.contact.fullName) }}</span>
</VAvatar>
</VBadge>
<div class="flex-grow-1 overflow-hidden">
<h6 class="text-h6 font-weight-regular">
{{ store.activeChat.contact.fullName }}
</h6>
<p class="text-body-2 text-truncate mb-0">
{{ store.activeChat.contact.role }}
</p>
</div>
</div>
<VSpacer />
<!-- Header right content -->
<div class="d-sm-flex align-center d-none">
<IconBtn>
<VIcon icon="ri-phone-line" />
</IconBtn>
<IconBtn>
<VIcon icon="ri-vidicon-line" />
</IconBtn>
<IconBtn>
<VIcon icon="ri-search-line" />
</IconBtn>
</div>
<MoreBtn :menu-list="moreList" />
</div>
<VDivider />
<!-- Chat log -->
<PerfectScrollbar
ref="chatLogPS"
tag="ul"
:options="{ wheelPropagation: false }"
class="flex-grow-1"
>
<ChatLog />
</PerfectScrollbar>
<!-- Message form -->
<VForm
class="chat-log-message-form mb-5 mx-5"
@submit.prevent="sendMessage"
>
<VTextField
:key="store.activeChat?.contact.id"
v-model="msg"
variant="solo"
density="default"
class="chat-message-input"
placeholder="Type your message..."
autofocus
>
<template #append-inner>
<IconBtn>
<VIcon icon="ri-mic-line" />
</IconBtn>
<IconBtn
class="me-4"
@click="refInputEl?.click()"
>
<VIcon icon="ri-attachment-2" />
</IconBtn>
<VBtn
append-icon="ri-send-plane-line"
@click="sendMessage"
>
Send
</VBtn>
</template>
</VTextField>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
>
</VForm>
</div>
<!-- 👉 Start conversation -->
<div
v-else
class="d-flex h-100 align-center justify-center flex-column"
>
<VAvatar
size="98"
color="primary"
variant="tonal"
class="mb-5"
>
<VIcon
size="50"
icon="ri-wechat-line"
/>
</VAvatar>
<p
class="mb-0 px-4 py-2 font-weight-medium elevation-2 rounded-xl bg-primary"
:class="[{ 'cursor-pointer': $vuetify.display.smAndDown }]"
@click="startConversation"
>
Start Conversation
</p>
</div>
</VMain>
</VLayout>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
@use "@core-scss/base/mixins.scss";
@use "@layouts/styles/mixins" as layoutsMixins;
// Variables
$chat-app-header-height: 76px;
// Placeholders
%chat-header {
display: flex;
align-items: center;
min-block-size: $chat-app-header-height;
padding-inline: 1.25rem;
}
.chat-app-layout {
border-radius: vuetify.$card-border-radius;
@include mixins.elevation(vuetify.$card-elevation);
$sel-chat-app-layout: &;
@at-root {
.skin--bordered {
@include mixins.bordered-skin($sel-chat-app-layout);
}
}
.active-chat-user-profile-sidebar,
.user-profile-sidebar {
.v-navigation-drawer__content {
display: flex;
flex-direction: column;
}
}
.chat-list-header,
.active-chat-header {
@extend %chat-header;
}
.chat-list-search {
.v-field__outline__start {
flex-basis: 20px !important;
border-radius: 28px 0 0 28px !important;
}
.v-field__outline__end {
border-radius: 0 28px 28px 0 !important;
}
@include layoutsMixins.rtl {
.v-field__outline__start {
flex-basis: 20px !important;
border-radius: 0 28px 28px 0 !important;
}
.v-field__outline__end {
border-radius: 28px 0 0 28px !important;
}
}
}
.chat-list-sidebar {
.v-navigation-drawer__content {
display: flex;
flex-direction: column;
}
}
}
.chat-content-container {
/* stylelint-disable-next-line value-keyword-case */
background-color: v-bind(chatContentContainerBg);
// Adjust the padding so text field height stays 48px
.chat-message-input {
.v-field__append-inner {
align-items: center;
padding-block-start: 0;
}
.v-field--appended {
padding-inline-end: 6px;
}
}
}
.chat-user-profile-badge {
.v-badge__badge {
/* stylelint-disable liberty/use-logical-spec */
min-width: 12px !important;
height: 0.75rem;
/* stylelint-enable liberty/use-logical-spec */
}
}
</style>

View File

@@ -0,0 +1,107 @@
<script setup>
import CustomerBioPanel from '@/views/apps/ecommerce/customer/view/CustomerBioPanel.vue'
import CustomerTabAddressAndBilling from '@/views/apps/ecommerce/customer/view/CustomerTabAddressAndBilling.vue'
import CustomerTabNotification from '@/views/apps/ecommerce/customer/view/CustomerTabNotification.vue'
import CustomerTabOverview from '@/views/apps/ecommerce/customer/view/CustomerTabOverview.vue'
import CustomerTabSecurity from '@/views/apps/ecommerce/customer/view/CustomerTabSecurity.vue'
const route = useRoute('apps-ecommerce-customer-details-id')
const customerData = ref()
const userTab = ref(null)
const tabs = [
{
icon: 'ri-group-line',
title: 'Overview',
},
{
icon: 'ri-lock-line',
title: 'Security',
},
{
icon: 'ri-map-pin-line',
title: 'Address & Billing',
},
{
icon: 'ri-notification-3-line',
title: 'Notifications',
},
]
const { data, error } = await useApi(`/apps/ecommerce/customers/${ route.params.id }`)
if (error.value)
console.log(error.value)
else if (data.value)
customerData.value = data.value
</script>
<template>
<div>
<!-- 👉 Header -->
<div class="d-flex justify-space-between align-center flex-wrap gap-y-4 mb-6">
<div>
<h4 class="text-h4">
Customer ID #{{ route.params.id }}
</h4>
<p class="text-body-1 mb-0">
Aug 17, 2020, 5:48 (ET)
</p>
</div>
<VBtn
variant="outlined"
color="error"
>
Delete Customer
</VBtn>
</div>
<!-- 👉 Customer Profile -->
<VRow v-if="customerData">
<VCol
cols="12"
md="5"
lg="4"
>
<CustomerBioPanel :customer-data="customerData" />
</VCol>
<VCol
cols="12"
md="7"
lg="8"
>
<VTabs
v-model="userTab"
class="v-tabs-pill mb-6 6 disable-tab-transition"
>
<VTab
v-for="tab in tabs"
:key="tab.icon"
>
<VIcon
start
:icon="tab.icon"
/>
<span>{{ tab.title }}</span>
</VTab>
</VTabs>
<VWindow
v-model="userTab"
class="mb-6 disable-tab-transition"
:touch="false"
>
<VWindowItem>
<CustomerTabOverview />
</VWindowItem>
<VWindowItem>
<CustomerTabSecurity />
</VWindowItem>
<VWindowItem>
<CustomerTabAddressAndBilling />
</VWindowItem>
<VWindowItem>
<CustomerTabNotification />
</VWindowItem>
</VWindow>
</VCol>
</VRow>
</div>
</template>

View File

@@ -0,0 +1,187 @@
<script setup>
import ECommerceAddCustomerDrawer from '@/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue'
const searchQuery = ref('')
const isAddCustomerDrawerOpen = ref(false)
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
// Data table Headers
const headers = [
{
title: 'Customer',
key: 'customer',
},
{
title: 'Customer Id',
key: 'customerId',
},
{
title: 'Country',
key: 'country',
},
{
title: 'Orders',
key: 'orders',
},
{
title: 'Total Spent',
key: 'totalSpent',
},
]
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const { data: customerData } = await useApi(createUrl('/apps/ecommerce/customers', {
query: {
q: searchQuery,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const customers = computed(() => customerData.value.customers)
const totalCustomers = computed(() => customerData.value.total)
</script>
<template>
<div>
<VCard>
<VCardText>
<div class="d-flex justify-space-between flex-wrap gap-y-4">
<VTextField
v-model="searchQuery"
style="max-inline-size: 200px; min-inline-size: 200px;"
density="compact"
placeholder="Search .."
/>
<div class="d-flex flex-row gap-4 align-center flex-wrap">
<VBtn
prepend-icon="ri-upload-2-line"
variant="outlined"
color="secondary"
>
Export
</VBtn>
<VBtn
prepend-icon="ri-add-line"
@click="isAddCustomerDrawerOpen = !isAddCustomerDrawerOpen"
>
Add Customer
</VBtn>
</div>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:items="customers"
item-value="customer"
:headers="headers"
:items-length="totalCustomers"
show-select
class="text-no-wrap"
@update:options="updateOptions"
>
<template #item.customer="{ item }">
<div class="d-flex align-center gap-x-3">
<VAvatar
size="34"
:image="item.avatar"
/>
<div class="d-flex flex-column">
<RouterLink
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: item.customerId } }"
class="text-h6 font-weight-medium"
>
{{ item.customer }}
</RouterLink>
<span class="text-sm">{{ item.email }}</span>
</div>
</div>
</template>
<template #item.customerId="{ item }">
<h6 class="text-h6 font-weight-regular">
#{{ item.customerId }}
</h6>
</template>
<template #item.orders="{ item }">
{{ item.order }}
</template>
<template #item.country="{ item }">
<div class="d-flex gap-x-2">
<img
:src="item.countryFlag"
height="22"
width="22"
>
<span class="text-body-1">{{ item.country }}</span>
</div>
</template>
<template #item.totalSpent="{ item }">
<h6 class="text-h6">
${{ item.totalSpent }}
</h6>
</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 }, totalCustomers) }}
</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(totalCustomers / itemsPerPage)"
@click="page >= Math.ceil(totalCustomers / itemsPerPage) ? page = Math.ceil(totalCustomers / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
<ECommerceAddCustomerDrawer v-model:is-drawer-open="isAddCustomerDrawerOpen" />
</div>
</template>

View File

@@ -0,0 +1,587 @@
<script setup>
const selectedStatus = ref('All')
const searchQuery = ref('')
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const {
data: ReviewData,
execute: fetchReviews,
} = await useApi(createUrl('/apps/ecommerce/reviews', {
query: {
q: searchQuery,
status: selectedStatus,
page,
itemsPerPage,
sortBy,
orderBy,
},
}))
const reviews = computed(() => ReviewData.value.reviews)
const totalReviews = computed(() => ReviewData.value.total)
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const deleteReview = async id => {
await $api(`/apps/ecommerce/reviews/${ id }`, { method: 'DELETE' })
fetchReviews()
}
const reviewCardData = [
{
rating: 5,
value: 124,
},
{
rating: 4,
value: 40,
},
{
rating: 3,
value: 12,
},
{
rating: 2,
value: 7,
},
{
rating: 1,
value: 2,
},
]
const headers = [
{
title: 'Product',
key: 'product',
},
{
title: 'Reviewer',
key: 'reviewer',
},
{
title: 'Review',
key: 'review',
sortable: false,
},
{
title: 'Date',
key: 'date',
},
{
title: 'Status',
key: 'status',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const labelColor = 'rgba(var(--v-theme-on-surface), var(--v-disabled-opacity))'
const reviewStatChartSeries = [{
data: [
20,
40,
60,
80,
100,
80,
60,
],
}]
const reviewStatChartConfig = {
chart: {
height: 160,
width: 190,
type: 'bar',
toolbar: { show: false },
},
legend: { show: false },
grid: {
show: false,
padding: {
top: -25,
bottom: -12,
},
},
colors: [
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
'rgba(var(--v-theme-success), 1)',
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
],
plotOptions: {
bar: {
barHeight: '75%',
columnWidth: '35%',
borderRadius: 5,
distributed: true,
},
},
dataLabels: { enabled: false },
xaxis: {
categories: [
'M',
'T',
'W',
'T',
'F',
'S',
'S',
],
axisBorder: { show: false },
axisTicks: { show: false },
labels: {
style: {
colors: labelColor,
fontSize: '13px',
},
},
},
yaxis: { labels: { show: false } },
responsive: [
{
breakpoint: 0,
options: {
chart: { width: '100%' },
plotOptions: { bar: { columnWidth: '40%' } },
},
},
{
breakpoint: 1440,
options: {
chart: {
height: 150,
width: 190,
toolbar: { show: !1 },
},
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 1400,
options: {
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 1200,
options: {
chart: {
height: 130,
width: 190,
toolbar: { show: !1 },
},
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 992,
chart: {
height: 150,
width: 190,
toolbar: { show: !1 },
},
options: {
plotOptions: {
bar: {
borderRadius: 5,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 883,
options: {
plotOptions: {
bar: {
borderRadius: 5,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 768,
options: {
chart: {
height: 150,
width: 190,
toolbar: { show: !1 },
},
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '40%',
},
},
},
},
{
breakpoint: 576,
options: {
chart: {
width: '100%',
height: '200',
type: 'bar',
},
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '30% ',
},
},
},
},
{
breakpoint: 420,
options: {
plotOptions: {
chart: {
width: '100%',
height: '200',
type: 'bar',
},
bar: {
borderRadius: 3,
columnWidth: '30%',
},
},
},
},
],
}
</script>
<template>
<VRow class="match-height">
<VCol
cols="12"
md="6"
>
<!-- 👉 Total Review Card -->
<VCard>
<VCardText>
<VRow>
<VCol
cols="12"
sm="6"
>
<div :class="$vuetify.display.smAndUp ? 'border-e' : 'border-b'">
<div class="d-flex align-center gap-x-2">
<h4 class="text-h3 text-primary">
4.89
</h4>
<VIcon
icon="ri-star-smile-line"
color="primary"
size="32"
/>
</div>
<h6 class="my-2 text-h6">
Total 187 reviews
</h6>
<div class="mb-2">
All reviews are from genuine customers
</div>
<VChip
color="primary"
size="small"
:class="$vuetify.display.smAndUp ? '' : 'mb-4'"
>
+5 This week
</VChip>
</div>
</VCol>
<VCol
cols="12"
sm="6"
>
<div
v-for="(item, index) in reviewCardData"
:key="index"
class="d-flex align-center gap-4 mb-3"
>
<div class="text-sm text-no-wrap">
{{ item.rating }} Star
</div>
<div class="w-100">
<VProgressLinear
color="primary"
height="8"
:model-value="(item.value / 185) * 100"
rounded
/>
</div>
<div class="text-sm">
{{ item.value }}
</div>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="6"
>
<VCard>
<VCardText>
<VRow>
<VCol
cols="12"
sm="5"
>
<div>
<h5 class="text-h5 mb-2">
Reviews statistics
</h5>
<div class="mb-9">
<span class="me-2">12 New Reviews</span>
<VChip
color="success"
size="small"
>
+8.4%
</VChip>
</div>
<div>
<div class="text-high-emphasis text-body-1 mb-2">
<span class="text-success">87%</span> Positive Reviews
</div>
<div class="text-body-2">
Weekly Report
</div>
</div>
</div>
</VCol>
<VCol
cols="12"
sm="7"
>
<div class="d-flex justify-start justify-sm-end">
<VueApexCharts
id="shipment-statistics"
type="bar"
height="150"
:options="reviewStatChartConfig"
:series="reviewStatChartSeries"
/>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<VCard>
<VCardText>
<div class="d-flex justify-space-between flex-wrap gap-y-4">
<VTextField
v-model="searchQuery"
style="max-inline-size: 250px; min-inline-size: 200px;"
placeholder="Search"
density="compact"
/>
<div class="d-flex flex-row gap-4 align-center flex-wrap">
<VSelect
v-model="selectedStatus"
style="min-inline-size: 6.25rem;"
density="compact"
:items="[
{ title: 'All', value: 'All' },
{ title: 'Published', value: 'Published' },
{ title: 'Pending', value: 'Pending' },
]"
/>
<VBtn prepend-icon="ri-upload-2-line">
Export
</VBtn>
</div>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="headers"
:items="reviews"
show-select
:items-length="totalReviews"
item-value="id"
class="text-no-wrap rounded-0"
@update:options="updateOptions"
>
<template #item.product="{ item }">
<div class="d-flex gap-x-4 align-center">
<VAvatar
:image="item.productImage"
:size="38"
variant="tonal"
rounded
/>
<div class="d-flex flex-column">
<h6 class="text-h6">
{{ item.product }}
</h6>
<span class="text-sm text-wrap clamp-text">{{ item.companyName }}</span>
</div>
</div>
</template>
<template #item.reviewer="{ item }">
<div class="d-flex align-center gap-x-4">
<VAvatar
:image="item.avatar"
size="34"
/>
<div class="d-flex flex-column">
<RouterLink
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: 478426 } }"
class="font-weight-medium"
>
{{ item.reviewer }}
</RouterLink>
<span class="text-body-2">{{ item.email }}</span>
</div>
</div>
</template>
<template #item.review="{ item }">
<div class="py-4">
<VRating
:size="24"
readonly
:model-value="item.review"
/>
<h6 class="text-h6 mb-1">
{{ item.head }}
</h6>
<p class="text-sm text-medium-emphasis text-wrap mb-0">
{{ item.para }}
</p>
</div>
</template>
<template #item.date="{ item }">
<span class="text-body-1">{{ new Date(item.date).toDateString() }}</span>
</template>
<template #item.status="{ item }">
<VChip
:color="item.status === 'Published' ? 'success' : 'warning'"
size="small"
>
{{ item.status }}
</VChip>
</template>
<template #item.actions="{ item }">
<IconBtn size="small">
<VIcon icon="ri-more-2-line" />
<VMenu activator="parent">
<VList>
<VListItem
value="view"
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.id } }"
>
View
</VListItem>
<VListItem
value="delete"
@click="deleteReview(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 }, totalReviews) }}
</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(totalReviews / itemsPerPage)"
@click="page >= Math.ceil(totalReviews / itemsPerPage) ? page = Math.ceil(totalReviews / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
@use "@core-scss/template/libs/apex-chart.scss";
</style>

View File

@@ -0,0 +1,413 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import product21 from '@images/ecommerce-images/product-21.png'
import product22 from '@images/ecommerce-images/product-22.png'
import product23 from '@images/ecommerce-images/product-23.png'
import product24 from '@images/ecommerce-images/product-24.png'
const route = useRoute('apps-ecommerce-order-details-id')
const isConfirmDialogVisible = ref(false)
const isUserInfoEditDialogVisible = ref(false)
const isEditAddressDialogVisible = ref(false)
const headers = [
{
title: 'Product',
key: 'productName',
},
{
title: 'Price',
key: 'price',
},
{
title: 'Quantity',
key: 'quantity',
},
{
title: 'Total',
key: 'total',
sortable: false,
},
]
const orderData = [
{
productName: 'OnePlus 7 Pro',
productImage: product21,
brand: 'OnePlus',
price: 799,
quantity: 1,
},
{
productName: 'Magic Mouse',
productImage: product22,
brand: 'Apple',
price: 89,
quantity: 1,
},
{
productName: 'Wooden Chair',
productImage: product23,
brand: 'insofer',
price: 289,
quantity: 2,
},
{
productName: 'Air Jorden',
productImage: product24,
brand: 'Nike',
price: 299,
quantity: 2,
},
]
</script>
<template>
<div>
<div class="d-flex justify-space-between align-center flex-wrap gap-y-4 mb-6">
<div>
<div class="d-flex gap-2 align-center mb-2 flex-wrap">
<h5 class="text-h5">
Order #{{ route.params.id }}
</h5>
<div class="d-flex gap-x-2">
<VChip
variant="tonal"
color="success"
size="small"
>
Paid
</VChip>
<VChip
variant="tonal"
color="info"
size="small"
>
Ready to Pickup
</VChip>
</div>
</div>
<div>
<span class="text-body-1">
Aug 17, 2020, 5:48 (ET)
</span>
</div>
</div>
<VBtn
variant="outlined"
color="error"
@click="isConfirmDialogVisible = !isConfirmDialogVisible"
>
Delete Order
</VBtn>
</div>
<VRow>
<VCol
cols="12"
md="8"
>
<!-- 👉 Order Details -->
<VCard class="mb-6">
<VCardItem>
<template #title>
<h5 class="text-h5">
Order Details
</h5>
</template>
<template #append>
<span class="text-primary cursor-pointer">Edit</span>
</template>
</VCardItem>
<VDataTable
:headers="headers"
:items="orderData"
item-value="productName"
show-select
class="text-no-wrap"
>
<template #item.productName="{ item }">
<div class="d-flex gap-x-3">
<VAvatar
size="34"
variant="tonal"
:image="item.productImage"
rounded
/>
<div class="d-flex flex-column align-center">
<h6 class="text-h6">
{{ item.productName }}
</h6>
<span class="text-sm text-start align-self-start">
{{ item.brand }}
</span>
</div>
</div>
</template>
<template #item.price="{ item }">
<span>${{ item.price }}</span>
</template>
<template #item.total="{ item }">
<span>
${{ item.price * item.quantity }}
</span>
</template>
<template #bottom />
</VDataTable>
<VDivider />
<VCardText>
<div class="d-flex align-end flex-column">
<table class="text-high-emphasis">
<tbody>
<tr>
<td width="200px">
Subtotal:
</td>
<td class="font-weight-medium">
$2,093
</td>
</tr>
<tr>
<td>Shipping fee: </td>
<td class="font-weight-medium">
$2
</td>
</tr>
<tr>
<td>Tax: </td>
<td class="font-weight-medium">
$28
</td>
</tr>
<tr>
<td class="font-weight-medium">
Total:
</td>
<td class="font-weight-medium">
$2,113
</td>
</tr>
</tbody>
</table>
</div>
</VCardText>
</VCard>
<!-- 👉 Shipping Activity -->
<VCard title="Shipping Activity">
<VCardText>
<VTimeline
truncate-line="both"
align="start"
side="end"
line-inset="10"
line-color="primary"
density="compact"
class="v-timeline-density-compact"
>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Order was placed (Order ID: #32543)</span>
<span class="app-timeline-meta">Tuesday 11:29 AM</span>
</div>
<p class="app-timeline-text mb-0">
Your order has been placed successfully
</p>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Pick-up</span>
<span class="app-timeline-meta">Wednesday 11:29 AM</span>
</div>
<p class="app-timeline-text mb-0">
Pick-up scheduled with courier
</p>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Dispatched</span>
<span class="app-timeline-meta">Thursday 8:15 AM</span>
</div>
<p class="app-timeline-text mb-0">
Item has been picked up by courier.
</p>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Package arrived</span>
<span class="app-timeline-meta">Saturday 15:20 AM</span>
</div>
<p class="app-timeline-text mb-0">
Package arrived at an Amazon facility, NY
</p>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Dispatched for delivery</span>
<span class="app-timeline-meta">Today 14:12 PM</span>
</div>
<p class="app-timeline-text mb-0">
Package has left an Amazon facility , NY
</p>
</VTimelineItem>
<VTimelineItem
dot-color="primary"
size="x-small"
>
<div class="d-flex justify-space-between align-center mb-3">
<span class="app-timeline-title">Delivery</span>
</div>
<p class="app-timeline-text mb-0">
Package will be delivered by tomorrow
</p>
</VTimelineItem>
</VTimeline>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="4"
>
<!-- 👉 Customer Details -->
<VCard class="mb-6">
<VCardText class="d-flex flex-column gap-y-6">
<h5 class="text-h5">
Customer Details
</h5>
<div class="d-flex align-center">
<VAvatar
:image="avatar1"
class="me-3"
/>
<div>
<div class="text-body-1 text-high-emphasis font-weight-medium">
Shamus Tuttle
</div>
<span>Customer ID: #47389</span>
</div>
</div>
<div class="d-flex align-center">
<VAvatar
variant="tonal"
color="success"
class="me-3"
>
<VIcon icon="ri-shopping-cart-line" />
</VAvatar>
<h6 class="text-h6">
12 Orders
</h6>
</div>
<div class="d-flex flex-column gap-y-1">
<div class="d-flex justify-space-between gap-1 text-body-2">
<h6 class="text-h6">
Contact Info
</h6>
<span
class="text-base text-primary font-weight-medium cursor-pointer"
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
>
Edit
</span>
</div>
<span>Email: Sheldon88@yahoo.com</span>
<span>Mobile: +1 (609) 972-22-22</span>
</div>
</VCardText>
</VCard>
<!-- 👉 Shipping Address -->
<VCard class="mb-6">
<VCardText>
<div class="d-flex align-center justify-space-between gap-1 mb-6">
<div class="text-body-1 text-high-emphasis font-weight-medium">
Shipping Address
</div>
<span
class="text-base text-primary font-weight-medium cursor-pointer"
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
>Edit</span>
</div>
<div>
45 Rocker Terrace <br> Latheronwheel <br> KW5 8NW, London <br> UK
</div>
</VCardText>
</VCard>
<!-- 👉 Billing Address -->
<VCard>
<VCardText>
<div class="d-flex align-center justify-space-between gap-1 mb-3">
<div class="text-body-1 text-high-emphasis font-weight-medium">
Billing Address
</div>
<span
class="text-base text-primary font-weight-medium cursor-pointer"
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
>Edit</span>
</div>
<div>
45 Rocker Terrace <br> Latheronwheel <br> KW5 8NW, London <br> UK
</div>
<div class="mt-6">
<h6 class="text-h6 mb-1">
Mastercard
</h6>
<div class="text-base">
Card Number: ******4291
</div>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
<ConfirmDialog
v-model:isDialogVisible="isConfirmDialogVisible"
confirmation-question="Are you sure to cancel your Order?"
cancel-msg="Order cancelled!!"
cancel-title="Cancelled"
confirm-msg="Your order cancelled successfully."
confirm-title="Cancelled!"
/>
<UserInfoEditDialog v-model:isDialogVisible="isUserInfoEditDialogVisible" />
<AddEditAddressDialog v-model:isDialogVisible="isEditAddressDialogVisible" />
</div>
</template>

View File

@@ -0,0 +1,395 @@
<script setup>
import mastercard from '@images/logos/mastercard.png'
import paypal from '@images/logos/paypal.png'
const widgetData = ref([
{
title: 'Pending Payment',
value: 56,
icon: 'ri-calendar-2-line',
},
{
title: 'Completed',
value: 12689,
icon: 'ri-check-double-line',
},
{
title: 'Refunded',
value: 124,
icon: 'ri-wallet-3-line',
},
{
title: 'Failed',
value: 32,
icon: 'ri-error-warning-line',
},
])
const searchQuery = ref('')
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
// Data table Headers
const headers = [
{
title: 'Order',
key: 'order',
},
{
title: 'Date',
key: 'date',
},
{
title: 'Customers',
key: 'customers',
},
{
title: 'Payment',
key: 'payment',
sortable: false,
},
{
title: 'Status',
key: 'status',
},
{
title: 'Method',
key: 'method',
sortable: false,
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const resolvePaymentStatus = status => {
if (status === 1)
return {
text: 'Paid',
color: 'success',
}
if (status === 2)
return {
text: 'Pending',
color: 'warning',
}
if (status === 3)
return {
text: 'Cancelled',
color: 'secondary',
}
if (status === 4)
return {
text: 'Failed',
color: 'error',
}
}
const resolveStatus = status => {
if (status === 'Delivered')
return {
text: 'Delivered',
color: 'success',
}
if (status === 'Out for Delivery')
return {
text: 'Out for Delivery',
color: 'primary',
}
if (status === 'Ready to Pickup')
return {
text: 'Ready to Pickup',
color: 'info',
}
if (status === 'Dispatched')
return {
text: 'Dispatched',
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)
const deleteOrder = async id => {
await $api(`/apps/ecommerce/orders/${ id }`, { method: 'DELETE' })
fetchOrders()
}
</script>
<template>
<div>
<VCard class="mb-6">
<VCardText class="px-2">
<VRow>
<template
v-for="(data, index) in widgetData"
:key="index"
>
<VCol
cols="12"
sm="6"
md="3"
class="px-6"
>
<div
class="d-flex justify-space-between"
:class="$vuetify.display.xs ? 'product-widget' : $vuetify.display.sm ? index < 2 ? 'product-widget' : '' : ''"
>
<div class="d-flex flex-column gap-y-1">
<h4 class="text-h4">
{{ data.value }}
</h4>
<span class="text-base text-capitalize">
{{ data.title }}
</span>
</div>
<VAvatar
variant="tonal"
rounded
size="42"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? index !== widgetData.length - 1 : $vuetify.display.smAndUp ? index % 2 === 0 : false"
vertical
inset
length="100"
/>
</template>
</VRow>
</VCardText>
</VCard>
<VCard>
<VCardText>
<div class="d-flex justify-sm-space-between align-center justify-start flex-wrap gap-4">
<VTextField
v-model="searchQuery"
placeholder="Search Order"
density="compact"
style=" max-inline-size: 200px; min-inline-size: 200px;"
/>
<VBtn
prepend-icon="ri-upload-2-line"
variant="outlined"
color="secondary"
>
Export
</VBtn>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="headers"
:items="orders"
item-value="order"
:items-length="totalOrder"
show-select
class="text-no-wrap"
@update:options="updateOptions"
>
<!-- Order ID -->
<template #item.order="{ item }">
<RouterLink :to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }">
#{{ item.order }}
</RouterLink>
</template>
<!-- Date -->
<template #item.date="{ item }">
{{ new Date(item.date).toDateString() }}
</template>
<!-- Customers -->
<template #item.customers="{ item }">
<div class="d-flex align-center">
<VAvatar
size="34"
:variant="!item.avatar.length ? 'tonal' : undefined"
:rounded="1"
class="me-4"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span
v-else
class="font-weight-medium"
>{{ avatarText(item.customer) }}</span>
</VAvatar>
<div class="d-flex flex-column">
<RouterLink :to="{ name: 'pages-user-profile-tab', params: { tab: 'profile' } }">
<div class="text-high-emphasis font-weight-medium">
{{ item.customer }}
</div>
</RouterLink>
<span class="text-sm">{{ item.email }}</span>
</div>
</div>
</template>
<!-- Payments -->
<template #item.payment="{ item }">
<div
:class="`text-${resolvePaymentStatus(item.payment)?.color}`"
class="d-flex align-center font-weight-medium"
>
<VIcon
size="10"
icon="ri-circle-fill"
class="me-2"
/>
<span>{{ resolvePaymentStatus(item.payment)?.text }}</span>
</div>
</template>
<!-- Status -->
<template #item.status="{ item }">
<VChip
v-bind="resolveStatus(item.status)"
size="small"
/>
</template>
<!-- Method -->
<template #item.method="{ item }">
<div class="d-flex align-start gap-x-2">
<img :src="item.method === 'mastercard' ? mastercard : paypal">
<div>
<VIcon
icon="ri-more-line"
class="me-2"
/>
<span v-if="item.method === 'mastercard'">
{{ item.methodNumber }}
</span>
<span v-else>
@gmail.com
</span>
</div>
</div>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn size="small">
<VIcon icon="ri-more-2-line" />
<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>
</div>
</template>
<style lang="scss" scoped>
#customer-link{
&:hover{
color: '#000' !important
}
}
.product-widget{
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
padding-block-end: 1rem;
}
</style>

View File

@@ -0,0 +1,688 @@
<script setup>
import {
useDropZone,
useFileDialog,
useObjectUrl,
} from '@vueuse/core'
import { ref } from 'vue'
const optionCounter = ref(1)
const dropZoneRef = ref()
const fileData = ref([])
const { open, onChange } = useFileDialog({ accept: 'image/*' })
function onDrop(DroppedFiles) {
DroppedFiles?.forEach(file => {
if (file.type.slice(0, 6) !== 'image/') {
alert('Only image files are allowed')
return
}
fileData.value.push({
file,
url: useObjectUrl(file).value ?? '',
})
})
}
onChange(selectedFiles => {
if (!selectedFiles)
return
for (const file of selectedFiles) {
fileData.value.push({
file,
url: useObjectUrl(file).value ?? '',
})
}
})
useDropZone(dropZoneRef, onDrop)
const content = ref(`<p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. That's it. It's probably too much for real minimalists though.
</p>
<p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
</p>`)
const activeTab = ref('Restock')
const shippingList = [
{
desc: 'You\'ll be responsible for product delivery.Any damage or delay during shipping may cost you a Damage fee',
title: 'Fulfilled by Seller',
value: 'Fulfilled by Seller',
},
{
desc: 'Your product, Our responsibility.For a measly fee, we will handle the delivery process for you.',
title: 'Fulfilled by Company name',
value: 'Fulfilled by Company name',
},
]
const shippingType = ref('Fulfilled by Company name')
const deliveryType = ref('Worldwide delivery')
const selectedAttrs = ref([
'Biodegradable',
'Expiry Date',
])
const inventoryTabsData = [
{
icon: 'ri-add-line',
title: 'Restock',
value: 'Restock',
},
{
icon: 'ri-flight-takeoff-line',
title: 'Shipping',
value: 'Shipping',
},
{
icon: 'ri-map-pin-line',
title: 'Global Delivery',
value: 'Global Delivery',
},
{
icon: 'ri-attachment-2',
title: 'Attributes',
value: 'Attributes',
},
{
icon: 'ri-lock-unlock-line',
title: 'Advanced',
value: 'Advanced',
},
]
const inStock = ref(true)
const isTaxable = ref(true)
</script>
<template>
<div>
<div class="d-flex flex-wrap justify-space-between gap-4 mb-6">
<div class="d-flex flex-column justify-center">
<h4 class="text-h4">
Add a new product
</h4>
<span class="text-medium-emphasis">Orders placed across your store</span>
</div>
<div class="d-flex gap-4 align-center flex-wrap">
<VBtn
variant="outlined"
color="secondary"
>
Discard
</VBtn>
<VBtn
variant="outlined"
color="primary"
>
Save Draft
</VBtn>
<VBtn>Publish Product</VBtn>
</div>
</div>
<VRow>
<VCol md="8">
<!-- 👉 Product Information -->
<VCard
class="mb-6"
title="Product Information"
>
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
label="Product Name"
placeholder="iPhone 14"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="SKU"
placeholder="FXSK123U"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
label="Barcode"
placeholder="0123-4567"
/>
</VCol>
<VCol>
<VLabel class="mb-1">
Description (Optional)
</VLabel>
<TiptapEditor
v-model="content"
class="border rounded"
/>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- 👉 Product Image -->
<VCard class="mb-6">
<VCardItem>
<template #title>
Product Image
</template>
<template #append>
<div class="text-primary font-weight-medium cursor-pointer">
Add Media from URL
</div>
</template>
</VCardItem>
<VCardText>
<div class="flex">
<div class="w-full h-auto relative">
<div
ref="dropZoneRef"
class="cursor-pointer"
@click="() => open()"
>
<div
v-if="fileData.length === 0"
class="d-flex flex-column justify-center align-center gap-y-2 pa-12 border-dashed drop-zone"
>
<VAvatar
variant="tonal"
color="secondary"
rounded
>
<VIcon icon="ri-upload-2-line" />
</VAvatar>
<h4 class="text-h4">
Drag and Drop Your Image Here.
</h4>
<span class="text-disabled">or</span>
<VBtn variant="outlined">
Browse Images
</VBtn>
</div>
<div
v-else
class="d-flex justify-center align-center gap-3 pa-8 border-dashed drop-zone flex-wrap"
>
<VRow class="match-height w-100">
<template
v-for="(item, index) in fileData"
:key="index"
>
<VCol
cols="12"
sm="4"
>
<VCard
:ripple="false"
border
>
<VCardText
class="d-flex flex-column"
@click.stop
>
<VImg
:src="item.url"
width="200px"
height="150px"
class="w-100 mx-auto"
/>
<div class="mt-2">
<span class="clamp-text text-wrap">
{{ item.file.name }}
</span>
<span>
{{ item.file.size / 1000 }} KB
</span>
</div>
</VCardText>
<VCardActions>
<VBtn
variant="text"
block
@click.stop="fileData.splice(index, 1)"
>
Remove File
</VBtn>
</VCardActions>
</VCard>
</VCol>
</template>
</VRow>
</div>
</div>
</div>
</div>
</VCardText>
</VCard>
<!-- 👉 Variants -->
<VCard
title="Variants"
class="mb-6"
>
<VCardText>
<template
v-for="i in optionCounter"
:key="i"
>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
:items="['Size', 'Color', 'Weight', 'Smell']"
placeholder="Select Variant"
label="Select Variant"
/>
</VCol>
<VCol
cols="12"
md="8"
>
<VTextField
label="Variant Value"
type="number"
placeholder="Enter Variant Value"
/>
</VCol>
</VRow>
</template>
<VBtn
class="mt-6"
@click="optionCounter++"
>
Add another option
</VBtn>
</VCardText>
</VCard>
<!-- 👉 Inventory -->
<VCard
title="Inventory"
class="inventory-card"
>
<VCardText>
<VRow>
<VCol
cols="12"
md="4"
>
<VTabs
v-model="activeTab"
direction="vertical"
color="primary"
class="v-tabs-pill"
>
<VTab
v-for="(tab, index) in inventoryTabsData"
:key="index"
:value="tab.value"
>
<VIcon
:icon="tab.icon"
class="me-2"
/>
<span>{{ tab.title }}</span>
</VTab>
</VTabs>
</VCol>
<VDivider
:vertical="$vuetify.display.mdAndUp ? true : false"
inset
/>
<VCol
cols="12"
md="8"
>
<VWindow
v-model="activeTab"
class="w-100"
:touch="false"
>
<VWindowItem value="Restock">
<div class="d-flex flex-column gap-y-4">
<div class="text-body-1 font-weight-medium">
Options
</div>
<div class="d-flex gap-x-4 align-center">
<VTextField
label="Add to stock"
placeholder="100"
density="compact"
/>
<VBtn prepend-icon="ri-check-line">
Confirm
</VBtn>
</div>
<div class="d-flex flex-column gap-2 text-high-emphasis">
<div>
Product in stock now: 54
</div>
<div>
Product in transit: 390
</div>
<div>
Last time restocked: 24th June, 2022
</div>
<div>
Total stock over lifetime: 2,430
</div>
</div>
</div>
</VWindowItem>
<VWindowItem value="Shipping">
<VRadioGroup v-model="shippingType">
<template #label>
<span class="font-weight-medium mb-1">Shipping Type</span>
</template>
<VRadio
v-for="item in shippingList"
:key="item.value"
:value="item.value"
class="mb-4 ps-1"
inline
>
<template #label>
<div>
<div class="text-high-emphasis font-weight-medium mb-1">
{{ item.title }}
</div>
<div class="text-sm">
{{ item.desc }}
</div>
</div>
</template>
</VRadio>
</VRadioGroup>
</VWindowItem>
<VWindowItem value="Global Delivery">
<div>
<VRadioGroup v-model="deliveryType">
<template #label>
<span class="font-weight-medium mb-1">Global Delivery</span>
</template>
<VRadio
value="Worldwide delivery"
class="mb-4 ps-1"
>
<template #label>
<div>
<div class="text-high-emphasis font-weight-medium mb-1">
Worldwide delivery
</div>
<div class="text-sm">
Only available with Shipping method:
<span class="text-primary">
Fulfilled by Company name
</span>
</div>
</div>
</template>
</VRadio>
<VRadio
value="Selected Countries"
class="mb-4 ps-1"
>
<template #label>
<div>
<div class="text-high-emphasis font-weight-medium mb-1">
Selected Countries
</div>
<VTextField
placeholder="USA"
style="min-inline-size: 200px;"
/>
</div>
</template>
</VRadio>
<VRadio>
<template #label>
<div>
<div class="text-high-emphasis font-weight-medium mb-1">
Local delivery
</div>
<div class="text-sm">
Deliver to your country of residence
<span class="text-primary">
Change profile address
</span>
</div>
</div>
</template>
</VRadio>
</VRadioGroup>
</div>
</VWindowItem>
<VWindowItem value="Attributes">
<div class="mb-2 text-base font-weight-medium">
Attributes
</div>
<div>
<VCheckbox
v-model="selectedAttrs"
label="Fragile Product"
value="Fragile Product"
class="ps-3"
/>
<VCheckbox
v-model="selectedAttrs"
value="Biodegradable"
label="Biodegradable"
class="ps-3"
/>
<VCheckbox
v-model="selectedAttrs"
value="Frozen Product"
class="ps-3"
>
<template #label>
<div class="d-flex flex-column mb-1">
<div>Frozen Product</div>
<VTextField
placeholder="40 C"
type="number"
style="min-inline-size: 250px;"
/>
</div>
</template>
</VCheckbox>
<VCheckbox
v-model="selectedAttrs"
value="Expiry Date"
class="ps-3"
>
<template #label>
<div class="d-flex flex-column mb-1">
<div>Expiry Date of Product</div>
<AppDateTimePicker
model-value="2025-06-14"
placeholder="Select a Date"
/>
</div>
</template>
</VCheckbox>
</div>
</VWindowItem>
<VWindowItem value="Advanced">
<div class="mb-3 text-base font-weight-medium">
Advanced
</div>
<VRow>
<VCol
cols="12"
sm="6"
md="7"
>
<VSelect
style="min-inline-size: 200px;"
label="Product ID Type"
placeholder="Select Product Type"
:items="['ISBN', 'UPC', 'EAN', 'JAN']"
/>
</VCol>
<VCol
cols="12"
sm="6"
md="5"
>
<VTextField
label="Product Id"
placeholder="100023"
type="number"
/>
</VCol>
</VRow>
</VWindowItem>
</VWindow>
</VCol>
</VRow>
</VCardText>
</VCard>
</VCol>
<VCol
md="4"
cols="12"
>
<!-- 👉 Pricing -->
<VCard
title="Pricing"
class="mb-6"
>
<VCardText>
<VTextField
label="Base Price"
placeholder="iPhone 14"
class="mb-6"
/>
<VTextField
label="Discounted Price"
placeholder="$499"
class="mb-4"
/>
<VCheckbox
v-model="isTaxable"
label="Charge Tax on this product"
/>
<VDivider class="my-2" />
<div class="d-flex flex-raw align-center justify-space-between ">
<span>In stock</span>
<VSwitch
v-model="inStock"
density="compact"
/>
</div>
</VCardText>
</VCard>
<!-- 👉 Organize -->
<VCard title="Organize">
<VCardText>
<div class="d-flex flex-column gap-y-4">
<VSelect
placeholder="Select Vendor"
label="Vendor"
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
/>
<VSelect
placeholder="Select Category"
label="Category"
:items="['Household', 'Office', 'Electronics', 'Management', 'Automotive']"
>
<template #append>
<IconBtn
icon="ri-add-line"
variant="outlined"
color="primary"
rounded
/>
</template>
</VSelect>
<VSelect
placeholder="Select Collection"
label="Collection"
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
/>
<VSelect
placeholder="Select Status"
label="Status"
:items="['Published', 'Inactive', 'Scheduled']"
/>
<VTextField
label="Tags"
placeholder="Fashion, Trending, Summer"
/>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
</template>
<style lang="scss" scoped>
.drop-zone {
border: 1px dashed rgba(var(--v-theme-on-surface), 0.12);
border-radius: 6px;
}
</style>
<style lang="scss">
.inventory-card{
.v-radio-group,
.v-checkbox {
.v-selection-control {
align-items: start !important;
}
.v-label.custom-input {
border: none !important;
}
}
}
.ProseMirror{
p{
margin-block-end: 0;
}
padding: 0.5rem;
outline: none;
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
}
</style>

View File

@@ -0,0 +1,310 @@
<script setup>
import ECommerceAddCategoryDrawer from '@/views/apps/ecommerce/EcommerceAddCategoryDrawer.vue'
import product1 from '@images/ecommerce-images/product-1.png'
import product10 from '@images/ecommerce-images/product-10.png'
import product11 from '@images/ecommerce-images/product-11.png'
import product12 from '@images/ecommerce-images/product-12.png'
import product14 from '@images/ecommerce-images/product-14.png'
import product17 from '@images/ecommerce-images/product-17.png'
import product19 from '@images/ecommerce-images/product-19.png'
import product2 from '@images/ecommerce-images/product-2.png'
import product25 from '@images/ecommerce-images/product-25.png'
import product28 from '@images/ecommerce-images/product-28.png'
import product9 from '@images/ecommerce-images/product-9.png'
const categoryData = [
{
categoryTitle: 'Smart Phone',
description: 'Choose from wide range of smartphones online at best prices.',
totalProduct: 12548,
totalEarning: 98784,
image: product1,
},
{
categoryTitle: 'Clothing, Shoes, and jewellery',
description: 'Fashion for a wide selection of clothing, shoes, jewellery and watches.',
totalProduct: 4689,
totalEarning: 45627,
image: product9,
},
{
categoryTitle: 'Home and Kitchen',
description: 'Browse through the wide range of Home and kitchen products.',
totalProduct: 12548,
totalEarning: 98784,
image: product10,
},
{
categoryTitle: 'Beauty and Personal Care',
description: 'Explore beauty and personal care products, shop makeup and etc.',
totalProduct: 12548,
totalEarning: 98784,
image: product19,
},
{
categoryTitle: 'Books',
description: 'Over 25 million titles across categories such as business and etc.',
totalProduct: 12548,
totalEarning: 98784,
image: product25,
},
{
categoryTitle: 'Games',
description: 'Every month, get exclusive in-game loot, free games, a free subscription.',
totalProduct: 12548,
totalEarning: 98784,
image: product12,
},
{
categoryTitle: 'Baby Products',
description: 'Buy baby products across different categories from top brands.',
totalProduct: 12548,
totalEarning: 98784,
image: product14,
},
{
categoryTitle: 'Growsari',
description: 'Shop grocery Items through at best prices in India.',
totalProduct: 12548,
totalEarning: 98784,
image: product28,
},
{
categoryTitle: 'Computer Accessories',
description: 'Enhance your computing experience with our range of computer accessories.',
totalProduct: 9876,
totalEarning: 65421,
image: product17,
},
{
categoryTitle: 'Fitness Tracker',
description: 'Monitor your health and fitness goals with our range of advanced fitness trackers.',
totalProduct: 1987,
totalEarning: 32067,
image: product10,
},
{
categoryTitle: 'Smart Home Devices',
description: 'Transform your home into a smart home with our innovative smart home devices.',
totalProduct: 2345,
totalEarning: 87654,
image: product11,
},
{
categoryTitle: 'Audio Speakers',
description: 'Immerse yourself in rich audio quality with our wide range of speakers.',
totalProduct: 5678,
totalEarning: 32145,
image: product2,
},
]
const headers = [
{
title: 'Categories',
key: 'categoryTitle',
},
{
title: 'Total Products',
key: 'totalProduct',
align: 'end',
},
{
title: 'Total Earning',
key: 'totalEarning',
align: 'end',
},
{
title: 'Action',
key: 'actions',
sortable: false,
},
]
const itemsPerPage = ref(10)
const searchQuery = ref('')
const isAddProductDrawerOpen = ref(false)
const page = ref(1)
const updateOptions = options => {
page.value = options.page
}
</script>
<template>
<div>
<VCard>
<VCardText>
<div class="d-flex justify-md-space-between flex-wrap gap-4 justify-center">
<VTextField
v-model="searchQuery"
placeholder="Search"
density="compact"
style="max-inline-size: 280px; min-inline-size: 200px;"
/>
<div class="d-flex align-center flex-wrap gap-4">
<VBtn
prepend-icon="ri-upload-2-line"
color="secondary"
variant="outlined"
>
Export
</VBtn>
<VBtn
prepend-icon="ri-add-line"
@click="isAddProductDrawerOpen = !isAddProductDrawerOpen"
>
Add Category
</VBtn>
</div>
</div>
</VCardText>
<VDataTable
v-model:items-per-page="itemsPerPage"
:headers="headers"
:page="page"
:items="categoryData"
item-value="categoryTitle"
:search="searchQuery"
show-select
class="text-no-wrap category-table"
@update:options="updateOptions"
>
<template #item.actions>
<IconBtn size="small">
<VIcon icon="ri-edit-box-line" />
</IconBtn>
<MoreBtn
size="small"
:menu-list="[
{
title: 'Download',
value: 'download',
prependIcon: 'ri-download-line',
},
{
title: 'Edit',
value: 'edit',
prependIcon: 'ri-pencil-line',
},
{
title: 'Duplicate',
value: 'duplicate',
prependIcon: 'ri-stack-line',
},
]"
item-props
/>
</template>
<template #item.categoryTitle="{ item }">
<div class="d-flex gap-x-3">
<VAvatar
variant="tonal"
rounded
size="38"
>
<img
:src="item.image"
:alt="item.categoryTitle"
width="38"
height="38"
>
</VAvatar>
<div>
<p class="text-high-emphasis font-weight-medium mb-0">
{{ item.categoryTitle }}
</p>
<div class="text-body-2">
{{ item.description }}
</div>
</div>
</div>
</template>
<template #item.totalEarning="{ item }">
<div class="text-end pe-4">
{{ (item.totalEarning).toLocaleString("en-IN", { style: "currency", currency: 'USD' }) }}
</div>
</template>
<template #item.totalProduct="{ item }">
<div class="text-end pe-4">
{{ (item.totalProduct).toLocaleString() }}
</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 }, categoryData.length) }}
</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(categoryData.length / itemsPerPage)"
@click="page >= Math.ceil(categoryData.length / itemsPerPage) ? page = Math.ceil(categoryData.length / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTable>
</VCard>
<ECommerceAddCategoryDrawer v-model:isDrawerOpen="isAddProductDrawerOpen" />
</div>
</template>
<style lang="scss">
.ProseMirror-focused{
border: none;
}
.category-table.v-table.v-data-table{
.v-table__wrapper{
table{
thead{
tr{
th.v-data-table-column--align-end{
.v-data-table-header__content{
flex-direction: row;
justify-content: end;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,518 @@
<script setup>
const widgetData = ref([
{
title: 'In-Store Sales',
value: '$5,345',
icon: 'ri-home-6-line',
desc: '5k orders',
change: 5.7,
},
{
title: 'Website Sales',
value: '$74,347',
icon: 'ri-computer-line',
desc: '21k orders',
change: 12.4,
},
{
title: 'Discount',
value: '$14,235',
icon: 'ri-gift-line',
desc: '6k orders',
},
{
title: 'Affiliate',
value: '$8,345',
icon: 'ri-money-dollar-circle-line',
desc: '150 orders',
change: -3.5,
},
])
const headers = [
{
title: 'Product',
key: 'product',
},
{
title: 'Category',
key: 'category',
},
{
title: 'Stock',
key: 'stock',
sortable: false,
},
{
title: 'SKU',
key: 'sku',
},
{
title: 'Price',
key: 'price',
},
{
title: 'QTY',
key: 'qty',
},
{
title: 'Status',
key: 'status',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const selectedStatus = ref()
const selectedCategory = ref()
const selectedStock = ref()
const searchQuery = ref('')
const status = ref([
{
title: 'Scheduled',
value: 'Scheduled',
},
{
title: 'Publish',
value: 'Published',
},
{
title: 'Inactive',
value: 'Inactive',
},
])
const categories = ref([
{
title: 'Accessories',
value: 'Accessories',
},
{
title: 'Home Decor',
value: 'Home Decor',
},
{
title: 'Electronics',
value: 'Electronics',
},
{
title: 'Shoes',
value: 'Shoes',
},
{
title: 'Office',
value: 'Office',
},
{
title: 'Games',
value: 'Games',
},
])
const stockStatus = ref([
{
title: 'In Stock',
value: true,
},
{
title: 'Out of Stock',
value: false,
},
])
// 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 resolveCategory = category => {
if (category === 'Accessories')
return {
color: 'error',
icon: 'ri-headphone-line',
}
if (category === 'Home Decor')
return {
color: 'info',
icon: 'ri-home-6-line',
}
if (category === 'Electronics')
return {
color: 'primary',
icon: 'ri-computer-line',
}
if (category === 'Shoes')
return {
color: 'success',
icon: 'ri-footprint-line',
}
if (category === 'Office')
return {
color: 'warning',
icon: 'ri-briefcase-line',
}
if (category === 'Games')
return {
color: 'primary',
icon: 'ri-gamepad-line',
}
}
const resolveStatus = statusMsg => {
if (statusMsg === 'Scheduled')
return {
text: 'Scheduled',
color: 'warning',
}
if (statusMsg === 'Published')
return {
text: 'Publish',
color: 'success',
}
if (statusMsg === 'Inactive')
return {
text: 'Inactive',
color: 'error',
}
}
const {
data: productsData,
execute: fetchProducts,
} = await useApi(createUrl('/apps/ecommerce/products', {
query: {
q: searchQuery,
stock: selectedStock,
category: selectedCategory,
status: selectedStatus,
page,
itemsPerPage,
sortBy,
orderBy,
},
}))
const products = computed(() => productsData.value.products)
const totalProduct = computed(() => productsData.value.total)
const deleteProduct = async id => {
await $api(`apps/ecommerce/products/${ id }`, { method: 'DELETE' })
fetchProducts()
}
</script>
<template>
<div>
<!-- 👉 widgets -->
<VCard class="mb-6">
<VCardText class="px-2">
<VRow>
<template
v-for="(data, index) in widgetData"
:key="index"
>
<VCol
cols="12"
sm="6"
md="3"
class="px-6"
>
<div
class="d-flex justify-space-between"
:class="$vuetify.display.xs ? 'product-widget' : $vuetify.display.sm ? index < 2 ? 'product-widget' : '' : ''"
>
<div class="d-flex flex-column gap-y-1">
<p class="text-capitalize mb-0">
{{ data.title }}
</p>
<h6 class="text-h4">
{{ data.value }}
</h6>
<div class="d-flex align-center">
<div class="text-no-wrap me-2">
{{ data.desc }}
</div>
<VChip
v-if="data.change"
size="small"
:color="data.change > 0 ? 'success' : 'error'"
>
{{ prefixWithPlus(data.change) }}%
</VChip>
</div>
</div>
<VAvatar
variant="tonal"
rounded
size="44"
>
<VIcon
:icon="data.icon"
size="28"
color="high-emphasis"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? index !== widgetData.length - 1 : $vuetify.display.smAndUp ? index % 2 === 0 : false"
vertical
inset
length="100"
/>
</template>
</VRow>
</VCardText>
</VCard>
<!-- 👉 products -->
<VCard title="Filters">
<VCardText>
<VRow>
<!-- 👉 Select Status -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedStatus"
label="Select Status"
placeholder="Select Status"
:items="status"
clearable
clear-icon="ri-close-line"
/>
</VCol>
<!-- 👉 Select Category -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedCategory"
label="Category"
placeholder="Select Category"
:items="categories"
clearable
clear-icon="ri-close-line"
/>
</VCol>
<!-- 👉 Select Stock Status -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedStock"
label="Stock"
placeholder="Stock"
:items="stockStatus"
clearable
clear-icon="ri-close-line"
/>
</VCol>
</VRow>
</VCardText>
<VDivider />
<VCardText class="d-flex flex-wrap gap-4">
<div class="d-flex align-center">
<!-- 👉 Search -->
<VTextField
v-model="searchQuery"
placeholder="Search Product"
style="inline-size: 200px;"
density="compact"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-x-4">
<!-- 👉 Export button -->
<div>
<VBtn
variant="outlined"
color="secondary"
prepend-icon="ri-external-link-line"
>
Export
</VBtn>
</div>
<VBtn
color="primary"
prepend-icon="ri-add-line"
@click="$router.push('/apps/ecommerce/product/add')"
>
Add Product
</VBtn>
</div>
</VCardText>
<!-- 👉 Datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="headers"
show-select
:items="products"
:items-length="totalProduct"
class="text-no-wrap rounded-0"
@update:options="updateOptions"
>
<!-- product -->
<template #item.product="{ item }">
<div class="d-flex align-center gap-x-4">
<VAvatar
v-if="item.image"
size="38"
variant="tonal"
rounded
:image="item.image"
/>
<div class="d-flex flex-column">
<span class="text-base text-high-emphasis font-weight-medium">{{ item.productName }}</span>
<span class="text-sm">{{ item.productBrand }}</span>
</div>
</div>
</template>
<!-- category -->
<template #item.category="{ item }">
<VAvatar
size="30"
variant="tonal"
:color="resolveCategory(item.category)?.color"
class="me-4"
>
<VIcon
:icon="resolveCategory(item.category)?.icon"
size="18"
/>
</VAvatar>
<span class="text-base text-high-emphasis">{{ item.category }}</span>
</template>
<!-- stock -->
<template #item.stock="{ item }">
<VSwitch :model-value="item.stock" />
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
v-bind="resolveStatus(item.status)"
size="small"
/>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn size="small">
<VIcon icon="ri-edit-box-line" />
</IconBtn>
<IconBtn size="small">
<VIcon icon="ri-more-2-fill" />
<VMenu activator="parent">
<VList>
<VListItem
value="download"
prepend-icon="ri-download-line"
>
Download
</VListItem>
<VListItem
value="delete"
prepend-icon="ri-delete-bin-line"
@click="deleteProduct(item.id)"
>
Delete
</VListItem>
<VListItem
value="duplicate"
prepend-icon="ri-stack-line"
>
Duplicate
</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 }, totalProduct) }}
</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(totalProduct / itemsPerPage)"
@click="page >= Math.ceil(totalProduct / itemsPerPage) ? page = Math.ceil(totalProduct / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</div>
</template>
<style lang="scss" scoped>
.product-widget{
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
padding-block-end: 1rem;
}
</style>

View File

@@ -0,0 +1,384 @@
<script setup>
import paperImg from '@images/svg/paper.svg?raw'
import rocketImg from '@images/svg/rocket.svg?raw'
import userInfoImg from '@images/svg/user-info.svg?raw'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
const rocketIcon = h('div', {
innerHTML: rocketImg,
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
})
const userInfoIcon = h('div', {
innerHTML: paperImg,
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
})
const paperIcon = h('div', {
innerHTML: userInfoImg,
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
})
const widgetData = [
{
title: 'Total Earning',
value: '$24,983',
icon: 'ri-money-dollar-circle-line',
color: 'primary',
},
{
title: 'Unpaid Earning',
value: '$8,647',
icon: 'ri-gift-line',
color: 'success',
},
{
title: 'Signups',
value: '2,367',
icon: 'ri-group-line',
color: 'error',
},
{
title: 'Conversion Rate',
value: '4.5%',
icon: 'ri-refresh-line',
color: 'info',
},
]
const stepsData = [
{
icon: rocketIcon,
desc: 'Create & validate your referral link and get',
value: '$50',
},
{
icon: paperIcon,
desc: 'For every new signup you get',
value: '10%',
},
{
icon: userInfoIcon,
desc: 'Get other friends to generate link and get',
value: '$100',
},
]
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
// Data Table Headers
const headers = [
{
title: 'Users',
key: 'users',
},
{
title: 'Referred ID',
key: 'referred-id',
},
{
title: 'Status',
key: 'status',
},
{
title: 'Value',
key: 'value',
},
{
title: 'Earnings',
key: 'earning',
},
]
const updateOptions = options => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const { data: referralData } = await useApi(createUrl('/apps/ecommerce/referrals', {
query: {
page,
itemsPerPage,
sortBy,
orderBy,
},
}))
const referrals = computed(() => referralData.value.referrals)
const totalReferrals = computed(() => referralData.value.total)
const resolveStatus = status => {
if (status === 'Rejected')
return {
text: 'Rejected',
color: 'error',
}
if (status === 'Unpaid')
return {
text: 'Unpaid',
color: 'warning',
}
if (status === 'Paid')
return {
text: 'Paid',
color: 'success',
}
}
</script>
<template>
<div>
<!-- 👉 Header -->
<VRow class="match-height">
<!-- 👉 Widgets -->
<VCol
v-for="(data, index) in widgetData"
:key="index"
cols="12"
md="3"
sm="6"
>
<VCard>
<VCardText>
<div class="d-flex align-center justify-space-between">
<div class="d-flex flex-column">
<span class="text-h5 mb-1">{{ data.value }}</span>
<span class="text-sm">{{ data.title }}</span>
</div>
<VAvatar
:icon="data.icon"
variant="tonal"
:color="data.color"
/>
</div>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Icon Steps -->
<VCol
cols="12"
md="8"
>
<VCard>
<VCardItem>
<VCardTitle> How to use</VCardTitle>
<VCardSubtitle>
Integrate your referral code in 3 easy steps.
</VCardSubtitle>
</VCardItem>
<VCardText>
<div class="d-flex gap-6 justify-space-between align-center flex-sm-row flex-column">
<div
v-for="(step, index) in stepsData"
:key="index"
class="d-flex flex-column align-center gap-y-2"
style="max-inline-size: 185px;"
>
<div class="icon-container">
<VNodeRenderer :nodes="step.icon" />
</div>
<span class="text-body-1 text-wrap text-center">
{{ step.desc }}
</span>
<span class="text-primary text-h6">{{ step.value }}</span>
</div>
</div>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Invite -->
<VCol
cols="12"
md="4"
>
<VCard>
<VCardText>
<div class="mb-11">
<h5 class="text-h5 mb-5">
Invite your friends
</h5>
<div class="d-flex align-center flex-wrap gap-4">
<VTextField
placeholder="Email Addresss"
density="compact"
/>
<VBtn>
<VIcon
start
icon="ri-check-line"
/>
Submit
</VBtn>
</div>
</div>
<div>
<h5 class="text-h5 mb-5">
Share the referral link
</h5>
<div class="d-flex align-center flex-wrap gap-4">
<VTextField
placeholder="themeselection.com/?ref=6478"
density="compact"
/>
<div class="d-flex gap-x-2">
<VBtn
icon
class="rounded"
color="#3B5998"
>
<VIcon
color="white"
icon="ri-facebook-circle-line"
/>
</VBtn>
<VBtn
icon
class="rounded"
color="#55ACEE"
>
<VIcon
color="white"
icon="ri-twitter-line"
/>
</VBtn>
</div>
</div>
</div>
</VCardText>
</VCard>
</VCol>
<!-- 👉 Referral Table -->
<VCol cols="12">
<VCard>
<VCardText>
<div class="d-flex justify-space-between align-center flex-wrap gap-4">
<h5 class="text-h5">
Referred Users
</h5>
<VBtn prepend-icon="ri-upload-2-line">
Export
</VBtn>
</div>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:items="referrals"
:headers="headers"
:items-length="totalReferrals"
show-select
class="text-high-emphasis"
@update:options="updateOptions"
>
<template #item.users="{ item }">
<div class="d-flex align-center gap-x-4">
<VAvatar
:image="item.avatar"
:size="34"
/>
<div>
<h6 class="text-h6">
<RouterLink
class="text-high-emphasis"
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: 478426 } }"
>
{{ item.user }}
</RouterLink>
</h6>
<div class="text-sm text-medium-emphasis">
{{ item.email }}
</div>
</div>
</div>
</template>
<template #item.referred-id="{ item }">
{{ item.referredId }}
</template>
<template #item.status="{ item }">
<VChip
v-bind="resolveStatus(item.status)"
size="small"
/>
</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 }, totalReferrals) }}
</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(totalReferrals / itemsPerPage)"
@click="page >= Math.ceil(totalReferrals / itemsPerPage) ? page = Math.ceil(totalReferrals / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
</VCol>
</VRow>
</div>
</template>
<style lang="scss" scoped>
.icon-container {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed rgb(var(--v-theme-primary));
border-radius: 50%;
block-size: 70px;
inline-size: 70px;
}
.icon {
color: #000;
font-size: 42px;
}
</style>

View File

@@ -0,0 +1,99 @@
<script setup>
import SettingsCheckout from '@/views/apps/ecommerce/settings/SettingsCheckout.vue'
import SettingsLocations from '@/views/apps/ecommerce/settings/SettingsLocations.vue'
import SettingsNotifications from '@/views/apps/ecommerce/settings/SettingsNotifications.vue'
import SettingsPayment from '@/views/apps/ecommerce/settings/SettingsPayment.vue'
import SettingsShippingAndDelivery from '@/views/apps/ecommerce/settings/SettingsShippingAndDelivery.vue'
import SettingsStoreDetails from '@/views/apps/ecommerce/settings/SettingsStoreDetails.vue'
const tabsData = [
{
icon: 'ri-store-line',
title: 'Store Details',
},
{
icon: 'ri-bank-card-line',
title: 'Payments',
},
{
icon: 'ri-shopping-cart-line',
title: 'Checkout',
},
{
icon: 'ri-car-line',
title: 'Shipping & Delivery',
},
{
icon: 'ri-map-pin-line',
title: 'Location',
},
{
icon: 'ri-notification-3-line',
title: 'Notifications',
},
]
const activeTab = ref(null)
</script>
<template>
<VRow>
<VCol
cols="12"
md="4"
>
<h5 class="text-h5 mb-4">
Getting Started
</h5>
<VTabs
v-model="activeTab"
direction="vertical"
class="v-tabs-pill disable-tab-transition"
>
<VTab
v-for="(tabItem, index) in tabsData"
:key="index"
:prepend-icon="tabItem.icon"
>
{{ tabItem.title }}
</VTab>
</VTabs>
</VCol>
<VCol
cols="12"
md="8"
>
<VWindow
v-model="activeTab"
class="disable-tab-transition"
:touch="false"
>
<VWindowItem>
<SettingsStoreDetails />
</VWindowItem>
<VWindowItem>
<SettingsPayment />
</VWindowItem>
<VWindowItem>
<SettingsCheckout />
</VWindowItem>
<VWindowItem>
<SettingsShippingAndDelivery />
</VWindowItem>
<VWindowItem>
<SettingsLocations />
</VWindowItem>
<VWindowItem>
<SettingsNotifications />
</VWindowItem>
</VWindow>
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,511 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import ComposeDialog from '@/views/apps/email/ComposeDialog.vue'
import EmailLeftSidebarContent from '@/views/apps/email/EmailLeftSidebarContent.vue'
import EmailView from '@/views/apps/email/EmailView.vue'
import { useEmail } from '@/views/apps/email/useEmail'
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
// Composables
const route = useRoute()
const { labels, resolveLabelColor, emailMoveToFolderActions, shallShowMoveToActionFor, moveSelectedEmailTo, updateEmails, updateEmailLabels } = useEmail()
// Compose dialog
const isComposeDialogVisible = ref(false)
// Ref
const q = ref('')
// Email Selection
// ------------------------------------------------
const selectedEmails = ref([])
const {
data: emailData,
execute: fetchEmails,
} = await useApi(createUrl('/apps/email', {
query: {
q,
filter: () => 'filter' in route.params ? route.params.filter : undefined,
label: () => 'label' in route.params ? route.params.label : undefined,
},
}))
const emails = computed(() => emailData.value.emails)
const toggleSelectedEmail = emailId => {
const emailIndex = selectedEmails.value.indexOf(emailId)
if (emailIndex === -1)
selectedEmails.value.push(emailId)
else
selectedEmails.value.splice(emailIndex, 1)
}
const selectAllEmailCheckbox = computed(() => emails.value.length && emails.value.length === selectedEmails.value.length)
const isSelectAllEmailCheckboxIndeterminate = computed(() => Boolean(selectedEmails.value.length) && emails.value.length !== selectedEmails.value.length)
const isAllMarkRead = computed(() => {
return selectedEmails.value.every(emailId => emails.value.find(email => email.id === emailId)?.isRead)
})
const selectAllCheckboxUpdate = () => {
selectedEmails.value = !selectAllEmailCheckbox.value ? emails.value.map(email => email.id) : []
}
// Email View
const openedEmail = ref(null)
const emailViewMeta = computed(() => {
const returnValue = {
hasNextEmail: false,
hasPreviousEmail: false,
}
if (openedEmail.value) {
const openedEmailIndex = emails.value.findIndex(e => e.id === openedEmail.value?.id)
returnValue.hasNextEmail = !!emails.value[openedEmailIndex + 1]
returnValue.hasPreviousEmail = !!emails.value[openedEmailIndex - 1]
}
return returnValue
})
const refreshOpenedEmail = async () => {
await fetchEmails()
if (openedEmail.value)
openedEmail.value = emails.value.find(e => e.id === openedEmail.value?.id)
}
/* You can optimize it so it doesn't fetch emails on each action.
Currently, if you just star the email, two API calls will get fired.
1. star the email
2. Fetch all latest emails
You can limit this to single API call by:
- making API to star the email
- modify the state (set that email's isStarred property to true/false) in the store instead of making API for fetching emails
😊 For simplicity of the code and possible of modification, we kept it simple.
*/
const handleActionClick = async (action, emailIds = selectedEmails.value) => {
selectedEmails.value = []
if (!emailIds.length)
return
if (action === 'trash')
await updateEmails(emailIds, { isDeleted: true })
else if (action === 'spam')
await updateEmails(emailIds, { folder: 'spam' })
else if (action === 'unread')
await updateEmails(emailIds, { isRead: false })
else if (action === 'read')
await updateEmails(emailIds, { isRead: true })
else if (action === 'star')
await updateEmails(emailIds, { isStarred: true })
else if (action === 'unstar')
await updateEmails(emailIds, { isStarred: false })
await fetchEmails()
if (openedEmail.value)
refreshOpenedEmail()
}
const handleMoveMailsTo = async action => {
moveSelectedEmailTo(action, selectedEmails.value)
await fetchEmails()
}
const changeOpenedEmail = dir => {
if (!openedEmail.value)
return
const openedEmailIndex = emails.value.findIndex(e => e.id === openedEmail.value?.id)
const newEmailIndex = dir === 'previous' ? openedEmailIndex - 1 : openedEmailIndex + 1
openedEmail.value = emails.value[newEmailIndex]
}
const openEmail = async email => {
openedEmail.value = email
await handleActionClick('read', [email.id])
}
watch(() => route.params, () => {
selectedEmails.value = []
}, { deep: true })
</script>
<template>
<VLayout
style="min-block-size: 100%;"
class="email-app-layout"
>
<VNavigationDrawer
v-model="isLeftSidebarOpen"
absolute
touchless
location="start"
:temporary="$vuetify.display.mdAndDown"
>
<EmailLeftSidebarContent @toggle-compose-dialog-visibility="isComposeDialogVisible = !isComposeDialogVisible" />
</VNavigationDrawer>
<EmailView
:email="openedEmail"
:email-meta="emailViewMeta"
@refresh="refreshOpenedEmail"
@navigated="changeOpenedEmail"
@close="openedEmail = null"
@remove="handleActionClick('trash', openedEmail ? [openedEmail.id] : [])"
@unread="handleActionClick('unread', openedEmail ? [openedEmail.id] : [])"
@star="handleActionClick('star', openedEmail ? [openedEmail.id] : [])"
@unstar="handleActionClick('unstar', openedEmail ? [openedEmail.id] : [])"
/>
<VMain>
<VCard
flat
class="email-content-list h-100 d-flex flex-column"
>
<div class="d-flex align-center">
<IconBtn
class="d-lg-none ms-3"
@click="isLeftSidebarOpen = true"
>
<VIcon icon="ri-menu-line" />
</IconBtn>
<!-- 👉 Search -->
<VTextField
v-model="q"
density="default"
class="email-search px-1 flex-grow-1"
placeholder="Search mail"
>
<template #prepend-inner>
<VIcon
color="disabled"
size="22"
icon="ri-search-line"
/>
</template>
</VTextField>
</div>
<VDivider />
<!-- 👉 Action bar -->
<div class="py-2 px-4 d-flex align-center">
<!-- TODO: Make checkbox primary on indeterminate state -->
<VCheckbox
:model-value="selectAllEmailCheckbox"
:indeterminate="isSelectAllEmailCheckboxIndeterminate"
class="me-1"
@update:model-value="selectAllCheckboxUpdate"
/>
<div
class="w-100 d-flex align-center action-bar-actions gap-1"
:style="{
visibility:
isSelectAllEmailCheckboxIndeterminate || selectAllEmailCheckbox
? undefined
: 'hidden',
}"
>
<!-- Trash -->
<IconBtn
v-show="('filter' in route.params ? route.params.filter !== 'trashed' : true)"
@click="handleActionClick('trash')"
>
<VIcon icon="ri-delete-bin-7-line" />
<VTooltip
activator="parent"
location="top"
>
Delete Mail
</VTooltip>
</IconBtn>
<!-- Mark unread/read -->
<IconBtn @click="isAllMarkRead ? handleActionClick('unread') : handleActionClick('read') ">
<VIcon :icon="isAllMarkRead ? 'ri-mail-line' : 'ri-mail-open-line'" />
<VTooltip
activator="parent"
location="top"
>
{{ isAllMarkRead ? 'Mark as Unread' : 'Mark as Read' }}
</VTooltip>
</IconBtn>
<!-- Move to folder -->
<IconBtn>
<VIcon icon="ri-folder-line" />
<VTooltip
activator="parent"
location="top"
>
Folder
</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="updateEmailLabels(selectedEmails, 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>
</div>
<VSpacer />
<IconBtn
class="me-1"
@click="fetchEmails"
>
<VIcon icon="ri-refresh-line" />
</IconBtn>
<MoreBtn />
</div>
<VDivider />
<!-- 👉 Emails list -->
<PerfectScrollbar
tag="ul"
:options="{ wheelPropagation: false }"
class="email-list"
>
<li
v-for="email in emails"
v-show="emails?.length"
:key="email.id"
class="email-item d-flex align-center pa-4 gap-2 cursor-pointer"
:class="[{ 'email-read': email.isRead }]"
@click="openEmail(email)"
>
<VCheckbox
:model-value="selectedEmails.includes(email.id)"
class="flex-shrink-0"
@update:model-value="toggleSelectedEmail(email.id)"
@click.stop
/>
<IconBtn
:color="email.isStarred ? 'warning' : 'default'"
@click.stop=" handleActionClick(email.isStarred ? 'unstar' : 'star', [email.id])"
>
<VIcon icon="ri-star-line" />
</IconBtn>
<VAvatar size="32">
<VImg
:src="email.from.avatar"
:alt="email.from.name"
/>
</VAvatar>
<h6 class="text-h6">
{{ email.from.name }}
</h6>
<span class="text-body-2 truncate">{{ email.subject }}</span>
<VSpacer />
<!-- 👉 Email meta -->
<div
class="email-meta align-center gap-2"
:class="$vuetify.display.xs ? 'd-none' : ''"
>
<VIcon
v-for="label in email.labels"
:key="label"
icon="ri-circle-fill"
size="10"
:color="resolveLabelColor(label)"
/>
<span class="text-sm text-disabled">
{{ formatDateToMonthShort(email.time) }}
</span>
</div>
<!-- 👉 Email actions -->
<div class="email-actions d-none">
<IconBtn @click.stop="handleActionClick('trash', [email.id])">
<VIcon icon="ri-delete-bin-7-line" />
<VTooltip
activator="parent"
location="top"
>
Delete Mail
</VTooltip>
</IconBtn>
<IconBtn
class="mx-2"
@click.stop=" handleActionClick(email.isRead ? 'unread' : 'read', [email.id])"
>
<VIcon :icon="email.isRead ? 'ri-mail-line' : 'ri-mail-open-line'" />
<VTooltip
activator="parent"
location="top"
>
{{ email.isRead ? 'Mark as Unread' : 'Mark as Read' }}
</VTooltip>
</IconBtn>
<IconBtn @click.stop="handleActionClick('spam', [email.id])">
<VIcon icon="ri-error-warning-line" />
<VTooltip
activator="parent"
location="top"
>
Move to Spam
</VTooltip>
</IconBtn>
</div>
</li>
<li
v-show="!emails.length"
class="py-4 px-5 text-center"
>
<span class="text-high-emphasis">No items found.</span>
</li>
</PerfectScrollbar>
</VCard>
<ComposeDialog
v-show="isComposeDialogVisible"
@close="isComposeDialogVisible = false"
/>
</VMain>
</VLayout>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
@use "@core-scss/base/mixins.scss";
// Remove border. Using variant plain cause UI issue, caret isn't align in center
.email-search {
.v-field__outline {
display: none;
}
}
.email-app-layout {
border-radius: vuetify.$card-border-radius;
@include mixins.elevation(vuetify.$card-elevation);
$sel-email-app-layout: &;
@at-root {
.skin--bordered {
@include mixins.bordered-skin($sel-email-app-layout);
}
}
}
.email-content-list {
border-end-start-radius: 0;
border-start-start-radius: 0;
}
.email-list {
white-space: nowrap;
.email-item {
transition: all 0.2s ease-in-out;
will-change: transform, box-shadow;
&.email-read {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
}
& + .email-item {
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
}
.email-item .email-meta {
display: flex;
}
.email-item:hover {
transform: translateY(-2px);
@include mixins.elevation(4);
// Don't show actions on hover on mobile & tablet devices
@media screen and (min-width: 1280px) {
.email-actions {
display: block !important;
}
.email-meta {
display: none;
}
}
+ .email-item {
border-color: transparent;
}
}
}
.email-compose-dialog {
position: absolute;
inset-block-end: 0;
inset-inline-end: 0;
min-inline-size: 100%;
@media only screen and (min-width: 800px) {
min-inline-size: 712px;
}
}
</style>

View File

@@ -0,0 +1,170 @@
<script setup>
import InvoiceEditable from '@/views/apps/invoice/InvoiceEditable.vue'
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
// 👉 Default Blank Data
const invoiceData = ref({
invoice: {
id: 5037,
issuedDate: '',
service: '',
total: 0,
avatar: '',
invoiceStatus: '',
dueDate: '',
balance: 0,
client: {
address: '112, Lorem Ipsum, Florida',
company: 'Greeva Inc',
companyEmail: 'johndoe@greeva.com',
contact: '+1 123 3452 12',
country: 'USA',
name: 'John Doe',
},
},
paymentDetails: {
totalDue: '$12,110.55',
bankName: 'American Bank',
country: 'United States',
iban: 'ETD95476213',
swiftCode: 'BR91905',
},
purchasedProducts: [{
title: '',
cost: 0,
hours: 0,
description: '',
}],
note: '',
paymentMethod: '',
salesperson: '',
thanksNote: '',
})
const paymentTerms = ref(true)
const clientNotes = ref(false)
const paymentStub = ref(false)
const selectedPaymentMethod = ref('Bank Account')
const paymentMethods = [
'Bank Account',
'PayPal',
'UPI Transfer',
]
const isSendPaymentSidebarVisible = ref(false)
const addProduct = value => {
invoiceData.value?.purchasedProducts.push(value)
}
const removeProduct = id => {
invoiceData.value?.purchasedProducts.splice(id, 1)
}
</script>
<template>
<VRow>
<!-- 👉 InvoiceEditable -->
<VCol
cols="12"
md="9"
>
<InvoiceEditable
:data="invoiceData"
@push="addProduct"
@remove="removeProduct"
/>
</VCol>
<!-- 👉 Right Column: Invoice Action -->
<VCol
cols="12"
md="3"
>
<VCard class="mb-8">
<VCardText>
<!-- 👉 Send Invoice -->
<VBtn
block
prepend-icon="ri-send-plane-line"
class="mb-3"
@click="isSendPaymentSidebarVisible = true"
>
Send Invoice
</VBtn>
<!-- 👉 Preview -->
<VBtn
block
color="secondary"
variant="outlined"
class="mb-3"
:to="{ name: 'apps-invoice-preview-id', params: { id: '5036' } }"
>
Preview
</VBtn>
<!-- 👉 Save -->
<VBtn
block
color="secondary"
variant="outlined"
>
Save
</VBtn>
</VCardText>
</VCard>
<!-- 👉 Select payment method -->
<VSelect
v-model="selectedPaymentMethod"
:items="paymentMethods"
label="Accept Payment Via"
class="mb-6"
/>
<!-- 👉 Payment Terms -->
<div class="d-flex align-center justify-space-between mb-2">
<VLabel for="payment-terms">
Payment Terms
</VLabel>
<div>
<VSwitch
id="payment-terms"
v-model="paymentTerms"
/>
</div>
</div>
<!-- 👉 Client Notes -->
<div class="d-flex align-center justify-space-between mb-2">
<VLabel for="client-notes">
Client Notes
</VLabel>
<div>
<VSwitch
id="client-notes"
v-model="clientNotes"
/>
</div>
</div>
<!-- 👉 Payment Stub -->
<div class="d-flex align-center justify-space-between">
<VLabel for="payment-stub">
Payment Stub
</VLabel>
<div>
<VSwitch
id="payment-stub"
v-model="paymentStub"
/>
</div>
</div>
</VCol>
</VRow>
<!-- 👉 Send Invoice Sidebar -->
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendPaymentSidebarVisible" />
</template>

View File

@@ -0,0 +1,166 @@
<script setup>
import InvoiceAddPaymentDrawer from '@/views/apps/invoice/InvoiceAddPaymentDrawer.vue'
import InvoiceEditable from '@/views/apps/invoice/InvoiceEditable.vue'
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
const invoiceData = ref()
const route = useRoute('apps-invoice-edit-id')
const { data: invoiceDetails } = await useApi(`/apps/invoice/${ route.params.id }`)
invoiceData.value = {
invoice: invoiceDetails.value.invoice,
paymentDetails: invoiceDetails.value.paymentDetails,
purchasedProducts: [{
title: 'App Design',
cost: 24,
hours: 2,
description: 'Designed UI kit & app pages.',
}],
note: 'It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance projects. Thank You!',
paymentMethod: 'Bank Account',
salesperson: 'Tom Cook',
thanksNote: 'Thanks for your business',
}
const addProduct = value => {
invoiceData.value?.purchasedProducts.push(value)
}
const removeProduct = id => {
invoiceData.value?.purchasedProducts.splice(id, 1)
}
const isSendSidebarActive = ref(false)
const isAddPaymentSidebarActive = ref(false)
const paymentTerms = ref(true)
const clientNotes = ref(false)
const paymentStub = ref(false)
const selectedPaymentMethod = ref('Bank Account')
const paymentMethods = [
'Bank Account',
'PayPal',
'UPI Transfer',
]
</script>
<template>
<VRow>
<!-- 👉 InvoiceEditable -->
<VCol
cols="12"
md="9"
>
<InvoiceEditable
v-if="invoiceData?.invoice"
:data="invoiceData"
@push="addProduct"
@remove="removeProduct"
/>
</VCol>
<!-- 👉 Right Column: Invoice Action -->
<VCol
cols="12"
md="3"
>
<VCard class="mb-8">
<VCardText>
<!-- 👉 Send Invoice Trigger button -->
<VBtn
block
prepend-icon="ri-send-plane-line"
class="mb-4"
@click="isSendSidebarActive = true"
>
Send Invoice
</VBtn>
<div class="d-flex flex-wrap gap-4">
<!-- 👉 Preview button -->
<VBtn
color="secondary"
variant="outlined"
class="flex-grow-1"
:to="{ name: 'apps-invoice-preview-id', params: { id: route.params.id } }"
>
Preview
</VBtn>
<!-- 👉 Save button -->
<VBtn
color="secondary"
variant="outlined"
class="mb-4 flex-grow-1"
>
Save
</VBtn>
</div>
<!-- 👉 Add Payment trigger button -->
<VBtn
block
color="success"
prepend-icon="ri-money-dollar-circle-line"
@click="isAddPaymentSidebarActive = true"
>
Add Payment
</VBtn>
</VCardText>
</VCard>
<!-- 👉 Accept payment via -->
<VSelect
v-model="selectedPaymentMethod"
:items="paymentMethods"
label="Accept Payment Via"
class="mb-6"
/>
<!-- 👉 Payment Terms -->
<div class="d-flex align-center justify-space-between mb-2">
<VLabel for="payment-terms">
Payment Terms
</VLabel>
<div>
<VSwitch
id="payment-terms"
v-model="paymentTerms"
/>
</div>
</div>
<!-- 👉 Client Notes -->
<div class="d-flex align-center justify-space-between mb-2">
<VLabel for="client-notes">
Client Notes
</VLabel>
<div>
<VSwitch
id="client-notes"
v-model="clientNotes"
/>
</div>
</div>
<!-- 👉 Payment Stub -->
<div class="d-flex align-center justify-space-between">
<VLabel for="payment-stub">
Payment Stub
</VLabel>
<div>
<VSwitch
id="payment-stub"
v-model="paymentStub"
/>
</div>
</div>
</VCol>
<!-- 👉 Invoice send drawer -->
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendSidebarActive" />
<!-- 👉 Invoice add payment drawer -->
<InvoiceAddPaymentDrawer v-model:isDrawerOpen="isAddPaymentSidebarActive" />
</VRow>
</template>

View File

@@ -0,0 +1,454 @@
<script setup>
const searchQuery = ref('')
const selectedStatus = ref(null)
const selectedRows = 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 widgetData = ref([
{
title: 'Clients',
value: 24,
icon: 'ri-user-line',
},
{
title: 'Invoices',
value: 165,
icon: 'ri-pages-line',
},
{
title: 'Paid',
value: '$2.46k',
icon: 'ri-wallet-line',
},
{
title: 'Unpaid',
value: '$876',
icon: 'ri-money-dollar-circle-line',
},
])
// 👉 headers
const headers = [
{
title: '#',
key: 'id',
},
{
title: 'Status',
key: 'trending',
sortable: false,
},
{
title: 'Client',
key: 'client',
},
{
title: 'Total',
key: 'total',
},
{
title: 'Issued Date',
key: 'date',
},
{
title: 'Balance',
key: 'balance',
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const {
data: invoiceData,
execute: fetchInvoices,
} = await useApi(createUrl('/apps/invoice', {
query: {
q: searchQuery,
status: selectedStatus,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const invoices = computed(() => invoiceData.value.invoices)
const totalInvoices = computed(() => invoiceData.value.totalInvoices)
// 👉 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">
<!-- 👉 Invoice Widgets -->
<VCard class="mb-6">
<VCardText class="px-2">
<VRow>
<template
v-for="(data, id) in widgetData"
:key="id"
>
<VCol
cols="12"
sm="6"
md="3"
class="px-6"
:class="id !== widgetData.length - 1 && $vuetify.display.width <= 600 ? 'border-b' : ''"
>
<div class="d-flex justify-space-between">
<div class="d-flex flex-column">
<h4 class="text-h4">
{{ data.value }}
</h4>
<span class="text-body-1 text-capitalize">{{ data.title }}</span>
</div>
<VAvatar
variant="tonal"
rounded
size="42"
>
<VIcon
:icon="data.icon"
size="26"
color="high-emphasis"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
: $vuetify.display.smAndUp ? id % 2 === 0
: false"
vertical
inset
/>
</template>
</VRow>
</VCardText>
</VCard>
<VCard id="invoice-list">
<VCardText class="d-flex align-center flex-wrap gap-4">
<!-- 👉 Create invoice -->
<VBtn
prepend-icon="ri-add-line"
:to="{ name: 'apps-invoice-add' }"
>
Create invoice
</VBtn>
<VSpacer />
<div class="d-flex align-center flex-wrap gap-4">
<!-- 👉 Search -->
<div class="invoice-list-search">
<VTextField
v-model="searchQuery"
density="compact"
placeholder="Search Invoice"
/>
</div>
<div class="invoice-list-search">
<VSelect
v-model="selectedStatus"
placeholder="Invoice Status"
clearable
density="compact"
clear-icon="ri-close-line"
:items="['Downloaded', 'Draft', 'Sent', 'Paid', 'Partial Payment', 'Past Due']"
/>
</div>
</div>
</VCardText>
<!-- SECTION Datatable -->
<VDataTableServer
v-model="selectedRows"
v-model:items-per-page="itemsPerPage"
v-model:page="page"
show-select
:items-length="totalInvoices"
:headers="headers"
:items="invoices"
item-value="id"
class="text-no-wrap rounded-0"
@update:options="updateOptions"
>
<!-- id -->
<template #item.id="{ item }">
<RouterLink :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
#{{ item.id }}
</RouterLink>
</template>
<!-- trending -->
<template #item.trending="{ item }">
<VTooltip>
<template #activator="{ props }">
<VAvatar
:size="28"
v-bind="props"
:color="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant"
variant="tonal"
>
<VIcon
:size="16"
:icon="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).icon"
/>
</VAvatar>
</template>
<p class="mb-0">
{{ item.invoiceStatus }}
</p>
<p class="mb-0">
Balance: {{ item.balance }}
</p>
<p class="mb-0">
Due date: {{ item.dueDate }}
</p>
</VTooltip>
</template>
<!-- client -->
<template #item.client="{ item }">
<div class="d-flex align-center">
<VAvatar
size="34"
:color="!item.avatar.length ? resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant : undefined"
:variant="!item.avatar.length ? 'tonal' : undefined"
class="me-3"
>
<VImg
v-if="item.avatar.length"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.client.name) }}</span>
</VAvatar>
<div class="d-flex flex-column">
<RouterLink
:to="{ name: 'pages-user-profile-tab', params: { tab: 'profile' } }"
class="text-h6 font-weight-medium mb-0"
>
{{ item.client.name }}
</RouterLink>
<span class="text-body-2">{{ item.client.companyEmail }}</span>
</div>
</div>
</template>
<!-- Total -->
<template #item.total="{ item }">
${{ item.total }}
</template>
<!-- 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"
size="small"
>
{{ (resolveInvoiceBalanceVariant(item.balance, item.total)).status }}
</VChip>
<h6
v-else
class="text-h6 font-weight-regular"
>
{{ Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status) > 0 ? `$${(resolveInvoiceBalanceVariant(item.balance, item.total)).status}` : `-$${Math.abs(Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status))}` }}
</h6>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<div class="text-no-wrap">
<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
/>
</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 }, 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>
<section v-else>
<VCard>
<VCardTitle>No Invoice Found</VCardTitle>
</VCard>
</section>
</template>
<style lang="scss">
#invoice-list {
.invoice-list-actions {
inline-size: 8rem;
}
.invoice-list-search {
inline-size: 12rem;
}
}
</style>

View File

@@ -0,0 +1,438 @@
<script setup>
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
import InvoiceAddPaymentDrawer from '@/views/apps/invoice/InvoiceAddPaymentDrawer.vue'
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
const route = useRoute('apps-invoice-preview-id')
const isAddPaymentSidebarVisible = ref(false)
const isSendPaymentSidebarVisible = ref(false)
const { data: invoiceData } = await useApi(`/apps/invoice/${ Number(route.params.id) }`)
const invoice = invoiceData.value.invoice
const paymentDetails = invoiceData.value.paymentDetails
const purchasedProducts = [
{
name: 'Premium Branding Package',
description: 'Branding & Promotion',
qty: 1,
hours: 15,
price: 32,
},
{
name: 'SMM',
description: 'Social media templates',
qty: 1,
hours: 14,
price: 28,
},
{
name: 'Web Design',
description: 'Web designing package',
qty: 1,
hours: 12,
price: 24,
},
{
name: 'SEO',
description: 'Search engine optimization',
qty: 1,
hours: 5,
price: 22,
},
]
const printInvoice = () => {
window.print()
}
</script>
<template>
<section v-if="invoiceData">
<VRow>
<VCol
cols="12"
md="9"
>
<VCard class="invoice-preview-wrapper pa-12">
<!-- SECTION Header -->
<div class="invoice-header-preview d-flex flex-wrap justify-space-between flex-column flex-sm-row print-row bg-var-theme-background gap-6 rounded 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="text-xl leading-normal text-uppercase">
{{ themeConfig.app.title }}
</h6>
</div>
<!-- 👉 Address -->
<h6 class="text-h6 font-weight-regular">
Office 149, 450 South Brand Brooklyn
</h6>
<h6 class="text-h6 font-weight-regular">
San Diego County, CA 91905, USA
</h6>
<h6 class="text-h6 font-weight-regular">
+1 (123) 456 7891, +44 (876) 543 2198
</h6>
</div>
<!-- 👉 Right Content -->
<div>
<!-- 👉 Invoice ID -->
<h6 class="font-weight-medium text-lg mb-6">
Invoice #{{ invoice.id }}
</h6>
<!-- 👉 Issue Date -->
<h6 class="text-h6 font-weight-regular">
<span>Date Issued: </span>
<span>{{ new Date(invoice.issuedDate).toLocaleDateString('en-GB') }}</span>
</h6>
<!-- 👉 Due Date -->
<h6 class="text-h6 font-weight-regular">
<span>Due Date: </span>
<span>{{ new Date(invoice.dueDate).toLocaleDateString('en-GB') }}</span>
</h6>
</div>
</div>
<!-- !SECTION -->
<!-- 👉 Payment Details -->
<VRow class="print-row mb-6">
<VCol class="text-no-wrap">
<h6 class="text-h6 mb-4">
Invoice To:
</h6>
<p class="mb-0">
{{ invoice.client.name }}
</p>
<p class="mb-0">
{{ invoice.client.company }}
</p>
<p 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>
{{ paymentDetails.totalDue }}
</td>
</tr>
<tr>
<td class="pe-6">
Bank Name:
</td>
<td>
{{ paymentDetails.bankName }}
</td>
</tr>
<tr>
<td class="pe-6">
Country:
</td>
<td>
{{ paymentDetails.country }}
</td>
</tr>
<tr>
<td class="pe-6">
IBAN:
</td>
<td>
{{ paymentDetails.iban }}
</td>
</tr>
<tr>
<td class="pe-6">
SWIFT Code:
</td>
<td>
{{ paymentDetails.swiftCode }}
</td>
</tr>
</tbody>
</table>
</VCol>
</VRow>
<!-- 👉 invoice Table -->
<VTable class="invoice-preview-table border text-high-emphasis overflow-hidden mb-6">
<thead>
<tr>
<th scope="col">
ITEM
</th>
<th scope="col">
DESCRIPTION
</th>
<th
scope="col"
class="text-center"
>
HOURS
</th>
<th
scope="col"
class="text-center"
>
QTY
</th>
<th
scope="col"
class="text-center"
>
TOTAL
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in purchasedProducts"
:key="item.name"
>
<td class="text-no-wrap">
{{ item.name }}
</td>
<td class="text-no-wrap">
{{ item.description }}
</td>
<td class="text-center">
{{ item.hours }}
</td>
<td class="text-center">
{{ item.qty }}
</td>
<td class="text-center">
${{ item.price }}
</td>
</tr>
</tbody>
</VTable>
<!-- 👉 Total -->
<div class="d-flex justify-space-between flex-column flex-sm-row print-row">
<div class="mb-2">
<div class="d-flex align-center mb-1">
<h6 class="text-h6 me-2">
Salesperson:
</h6>
<span>Jenny Parker</span>
</div>
<p>Thanks for your business</p>
</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-sm">
$1800
</h6>
</td>
</tr>
<tr>
<td class="pe-16">
Discount:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-sm">
$28
</h6>
</td>
</tr>
<tr>
<td class="pe-16">
Tax:
</td>
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
<h6 class="text-sm">
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-sm">
$1690
</h6>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<VDivider class="my-6 border-dashed" />
<p class="mb-0">
<span class="text-high-emphasis font-weight-medium me-1">
Note:
</span>
<span>It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance projects. Thank You!</span>
</p>
</VCard>
</VCol>
<VCol
cols="12"
md="3"
class="d-print-none"
>
<VCard>
<VCardText>
<!-- 👉 Send Invoice Trigger button -->
<VBtn
block
prepend-icon="ri-send-plane-line"
class="mb-4"
@click="isSendPaymentSidebarVisible = true"
>
Send Invoice
</VBtn>
<VBtn
block
color="secondary"
variant="outlined"
class="mb-4"
>
Download
</VBtn>
<div class="d-flex flex-wrap gap-4">
<VBtn
variant="outlined"
color="secondary"
class="flex-grow-1"
@click="printInvoice"
>
Print
</VBtn>
<VBtn
color="secondary"
variant="outlined"
class="mb-4 flex-grow-1"
:to="{ name: 'apps-invoice-edit-id', params: { id: route.params.id } }"
>
Edit
</VBtn>
</div>
<!-- 👉 Add Payment trigger button -->
<VBtn
block
prepend-icon="ri-money-dollar-circle-line"
color="success"
@click="isAddPaymentSidebarVisible = true"
>
Add Payment
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- 👉 Add Payment Sidebar -->
<InvoiceAddPaymentDrawer v-model:isDrawerOpen="isAddPaymentSidebarVisible" />
<!-- 👉 Send Invoice Sidebar -->
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendPaymentSidebarVisible" />
</section>
</template>
<style lang="scss">
.invoice-preview-table {
--v-table-header-color: var(--v-theme-surface);
&.v-table .v-table__wrapper table thead tr th{
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
}
}
@media print {
.v-theme--dark {
--v-theme-surface: 255, 255, 255;
--v-theme-on-surface: 94, 86, 105;
}
body {
background: none !important;
}
.invoice-header-preview,
.invoice-preview-wrapper {
padding: 0 !important;
}
.product-buy-now {
display: none;
}
.v-navigation-drawer,
.layout-vertical-nav,
.app-customizer-toggler,
.layout-footer,
.layout-navbar,
.layout-navbar-and-nav-container {
display: none;
}
.v-card {
box-shadow: none !important;
.print-row {
flex-direction: row !important;
}
}
.layout-content-wrapper {
padding-inline-start: 0 !important;
}
.v-table__wrapper {
overflow: hidden !important;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<script setup>
import LogisticsCardStatistics from '@/views/apps/logistics/LogisticsCardStatistics.vue'
import LogisticsDeliveryExpectations from '@/views/apps/logistics/LogisticsDeliveryExpectations.vue'
import LogisticsDeliveryPerformance from '@/views/apps/logistics/LogisticsDeliveryPerformance.vue'
import LogisticsOrderByCountries from '@/views/apps/logistics/LogisticsOrderByCountries.vue'
import LogisticsOverviewTable from '@/views/apps/logistics/LogisticsOverviewTable.vue'
import LogisticsShipmentStatistics from '@/views/apps/logistics/LogisticsShipmentStatistics.vue'
import LogisticsVehicleOverview from '@/views/apps/logistics/LogisticsVehicleOverview.vue'
</script>
<template>
<VRow class="match-height">
<VCol cols="12">
<LogisticsCardStatistics />
</VCol>
<VCol
cols="12"
md="6"
>
<LogisticsVehicleOverview />
</VCol>
<VCol
cols="12"
md="6"
>
<LogisticsShipmentStatistics />
</VCol>
<VCol
cols="12"
md="4"
>
<LogisticsDeliveryPerformance />
</VCol>
<VCol
cols="12"
md="4"
>
<LogisticsDeliveryExpectations />
</VCol>
<VCol
cols="12"
md="4"
>
<LogisticsOrderByCountries />
</VCol>
<VCol cols="12">
<LogisticsOverviewTable />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,378 @@
// ❗ WARNING please use your access token from mapbox.com
<script setup>
import mapboxgl from 'mapbox-gl'
import {
onMounted,
ref,
} from 'vue'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useDisplay } from 'vuetify'
import fleetImg from '@images/misc/fleet-car.png'
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
const accessToken = 'pk.eyJ1Ijoic29jaWFsZXhwbG9yZXIiLCJhIjoiREFQbXBISSJ9.dwFTwfSaWsHvktHrRtpydQ'
const map = ref()
const vuetifyDisplay = useDisplay()
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
const carImgs = ref([
fleetImg,
fleetImg,
fleetImg,
fleetImg,
])
const refCars = ref([])
const showPanel = ref([
true,
false,
false,
false,
])
const geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
-73.999024,
40.75249842,
],
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
-74.03,
40.75699842,
],
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
-73.967524,
40.7599842,
],
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
-74.0325,
40.742992,
],
},
},
],
}
const activeIndex = ref(0)
onMounted(() => {
mapboxgl.accessToken = accessToken
map.value = new mapboxgl.Map({
container: 'mapContainer',
style: 'mapbox://styles/mapbox/light-v9',
center: [
-73.999024,
40.75249842,
],
zoom: 12.25,
})
for (let index = 0; index < geojson.features.length; index++)
new mapboxgl.Marker({ element: refCars.value[index] }).setLngLat(geojson.features[index].geometry.coordinates).addTo(map.value)
refCars.value[activeIndex.value].classList.add('marker-focus')
})
const vehicleTrackingData = [
{
name: 'VOL-342808',
location: 'Chelsea, NY, USA',
progress: 88,
driverName: 'Veronica Herman',
},
{
name: 'VOL-954784',
location: 'Lincoln Harbor, NY, USA',
progress: 90,
driverName: 'Myrtle Ullrich',
},
{
name: 'VOL-342808',
location: 'Midtown East, NY, USA',
progress: 60,
driverName: 'Barry Schowalter',
},
{
name: 'VOL-343908',
location: 'Hoboken, NY, USA',
progress: 28,
driverName: 'Helen Jacobs',
},
]
const flyToLocation = (geolocation, index) => {
activeIndex.value = index
showPanel.value.fill(false)
showPanel.value[index] = !showPanel.value[index]
if (vuetifyDisplay.mdAndDown.value)
isLeftSidebarOpen.value = false
map.value.flyTo({
center: geolocation,
zoom: 16,
})
}
watch(activeIndex, () => {
refCars.value.forEach((car, index) => {
if (index === activeIndex.value)
car.classList.add('marker-focus')
else
car.classList.remove('marker-focus')
})
})
</script>
<template>
<VLayout class="fleet-app-layout">
<VNavigationDrawer
v-model="isLeftSidebarOpen"
width="320"
absolute
touchless
:border="0"
location="start"
>
<VCard
class="h-100"
flat
>
<VCardItem>
<VCardTitle>
Fleet
</VCardTitle>
<template #append>
<IconBtn
class="d-lg-none"
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
>
<VIcon icon="ri-close-line" />
</IconBtn>
</template>
</VCardItem>
<!-- 👉 Perfect Scrollbar -->
<PerfectScrollbar
:options="{ wheelPropagation: false, suppressScrollX: true }"
style="block-size: calc(100% - 60px);"
>
<VCardText class="pt-0">
<div
v-for="(vehicle, index) in vehicleTrackingData"
:key="index"
class="mb-6"
>
<div
class="d-flex align-center justify-space-between cursor-pointer"
@click="flyToLocation(geojson.features[index].geometry.coordinates, index)"
>
<div class="d-flex gap-x-4">
<VAvatar
icon="ri-car-line"
variant="tonal"
color="secondary"
/>
<div>
<h6 class="text-h6 font-weight-regular">
{{ vehicle.name }}
</h6>
<div class="text-body-1">
{{ vehicle.location }}
</div>
</div>
</div>
<IconBtn density="comfortable">
<VIcon :icon="showPanel[index] ? 'ri-arrow-down-s-line' : $vuetify.locale.isRtl ? 'ri-arrow-left-s-line' : 'ri-arrow-right-s-line'" />
</IconBtn>
</div>
<VExpandTransition mode="out-in">
<div v-show="showPanel[index]">
<div class="py-4 mb-4">
<div class="d-flex justify-space-between text-body-1 mb-1">
<span class="text-high-emphasis ">Delivery Process</span>
<span>{{ vehicle.progress }}%</span>
</div>
<VProgressLinear
:model-value="vehicle.progress"
color="primary"
rounded
height="6"
/>
</div>
<div>
<VTimeline
side="end"
align="start"
truncate-line="both"
density="compact"
class="v-timeline--variant-outlined"
>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgb(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
TRACKING NUMBER CREATED
</div>
<div class="app-timeline-title">
{{ vehicle.driverName }}
</div>
<div class="text-body-2 mb-1">
Sep 01, 7:53 AM
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-checkbox-circle-line"
dot-color="rgb(var(--v-theme-surface))"
icon-color="success"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-success">
OUT FOR DELIVERY
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="app-timeline-text mb-1">
Sep 03, 8:02 AM
</div>
</VTimelineItem>
<VTimelineItem
icon="ri-map-pin-line"
dot-color="rgb(var(--v-theme-surface))"
icon-color="primary"
fill-dot
size="20"
:elevation="0"
>
<div class="text-caption text-uppercase text-primary">
ARRIVED
</div>
<div class="app-timeline-title">
Veronica Herman
</div>
<div class="app-timeline-text">
Sep 04, 8:18 AM
</div>
</VTimelineItem>
</VTimeline>
</div>
</div>
</VExpandTransition>
</div>
</VCardText>
</PerfectScrollbar>
</VCard>
</VNavigationDrawer>
<VMain>
<div class="h-100">
<IconBtn
class="d-lg-none navigation-toggle-btn rounded-sm"
variant="elevated"
@click="isLeftSidebarOpen = true"
>
<VIcon icon="ri-menu-line" />
</IconBtn>
<!-- 👉 Fleet map -->
<div
id="mapContainer"
class="basemap"
/>
<img
v-for="(car, index) in carImgs"
:key="index"
ref="refCars"
:src="car"
alt="car Img marker"
height="42"
width="20"
>
</div>
</VMain>
</VLayout>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
@use "@core-scss/base/mixins.scss";
@import "mapbox-gl/dist/mapbox-gl.css";
.fleet-app-layout {
border-radius: vuetify.$card-border-radius;
@include mixins.elevation(vuetify.$card-elevation);
$sel-fleet-app-layout: &;
@at-root {
.skin--bordered {
@include mixins.bordered-skin($sel-fleet-app-layout);
}
}
}
.navigation-toggle-btn{
position: absolute;
z-index: 1;
inset-block-start: 1rem;
inset-inline-start: 1rem;
}
.navigation-close-btn{
position: absolute;
z-index: 1;
inset-block-start: 1rem;
inset-inline-end: 1rem;
}
.basemap {
block-size: 100%;
inline-size: 100%;
}
.marker-focus {
filter: drop-shadow(0 0 7px rgb(var(--v-theme-primary)));
}
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right {
display: none;
}
/* stylelint-disable-next-line selector-id-pattern */
#mapContainer {
block-size: 100vh !important;
}
</style>

View File

@@ -0,0 +1,203 @@
<script setup>
const headers = [
{
title: 'Name',
key: 'name',
},
{
title: 'Assigned To',
key: 'assignedTo',
sortable: false,
},
{
title: 'Created Date',
key: 'createdDate',
sortable: false,
},
{
title: 'Actions',
key: 'actions',
sortable: false,
},
]
const search = 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 isPermissionDialogVisible = ref(false)
const isAddPermissionDialogVisible = ref(false)
const permissionName = ref('')
const colors = {
'support': {
color: 'info',
text: 'Support',
},
'users': {
color: 'success',
text: 'Users',
},
'manager': {
color: 'warning',
text: 'Manager',
},
'administrator': {
color: 'primary',
text: 'Administrator',
},
'restricted-user': {
color: 'error',
text: 'Restricted User',
},
}
const { data: permissionsData } = await useApi(createUrl('/apps/permissions', {
query: {
q: search,
itemsPerPage,
page,
sortBy,
orderBy,
},
}))
const permissions = computed(() => permissionsData.value.permissions)
const totalPermissions = computed(() => permissionsData.value.totalPermissions)
const editPermission = name => {
isPermissionDialogVisible.value = true
permissionName.value = name
}
</script>
<template>
<VCard>
<VCardText class="d-flex align-center justify-sm-space-between justify-start gap-4 flex-wrap">
<VTextField
v-model="search"
density="compact"
placeholder="Search Permission"
style="max-inline-size: 15rem;min-inline-size: 12rem;"
/>
<VBtn
density="default"
@click="isAddPermissionDialogVisible = true"
>
Add Permission
</VBtn>
</VCardText>
<VDataTableServer
v-model:items-per-page="itemsPerPage"
:items-length="totalPermissions"
:items-per-page-options="[
{ value: 5, title: '5' },
{ value: 10, title: '10' },
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
]"
:headers="headers"
:items="permissions"
item-value="name"
class="text-no-wrap"
@update:options="updateOptions"
>
<!-- Assigned To -->
<template #item.assignedTo="{ item }">
<div class="d-flex gap-2">
<VChip
v-for="text in item.assignedTo"
:key="text"
:color="colors[text].color"
size="small"
>
{{ colors[text].text }}
</VChip>
</div>
</template>
<!-- Name -->
<template #item.name="{ item }">
<h6 class="text-h6 font-weight-regular">
{{ item.name }}
</h6>
</template>
<template #item.createdDate="{ item }">
<span class="text-body-1">{{ item.createdDate }}</span>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn
size="small"
@click="editPermission(item.name)"
>
<VIcon icon="ri-edit-box-line" />
</IconBtn>
<MoreBtn size="small" />
</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 }, totalPermissions) }}
</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(totalPermissions / itemsPerPage)"
@click="page >= Math.ceil(totalPermissions / itemsPerPage) ? page = Math.ceil(totalPermissions / itemsPerPage) : page++ "
/>
</div>
</div>
</template>
</VDataTableServer>
</VCard>
<AddEditPermissionDialog
v-model:isDialogVisible="isPermissionDialogVisible"
v-model:permission-name="permissionName"
/>
<AddEditPermissionDialog v-model:isDialogVisible="isAddPermissionDialogVisible" />
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import RoleCards from '@/views/apps/roles/RoleCards.vue'
import UserList from '@/views/apps/roles/UserList.vue'
</script>
<template>
<VRow>
<VCol cols="12">
<h5 class="text-h5 mb-1">
Roles List
</h5>
<p class="text-body-1 mb-0">
A role provided access to predefined menus and features so that depending on assigned role an administrator can have access to what he need
</p>
</VCol>
<!-- 👉 Roles Cards -->
<VCol cols="12">
<RoleCards />
</VCol>
<VCol cols="12">
<h5 class="text-h5 mt-6">
Total users with their roles
</h5>
<p class="text-body-1 mb-6">
Find all of your company's administrator accounts and their associate roles.
</p>
<!-- 👉 User List -->
<UserList />
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,533 @@
<script setup>
import AddNewUserDrawer from '@/views/apps/user/list/AddNewUserDrawer.vue'
// 👉 Store
const searchQuery = ref('')
const selectedRole = ref()
const selectedPlan = ref()
const selectedStatus = ref()
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
const 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 plans = [
{
title: 'Basic',
value: 'basic',
},
{
title: 'Company',
value: 'company',
},
{
title: 'Enterprise',
value: 'enterprise',
},
{
title: 'Team',
value: 'team',
},
]
const status = [
{
title: 'Pending',
value: 'pending',
},
{
title: 'Active',
value: 'active',
},
{
title: 'Inactive',
value: 'inactive',
},
]
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: 'success',
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 isAddNewUserDrawerVisible = ref(false)
const addNewUser = async userData => {
// userListStore.addUser(userData)
await $api('/apps/users', {
method: 'POST',
body: userData,
})
// refetch User
fetchUsers()
}
const deleteUser = async id => {
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
// refetch User
fetchUsers()
}
const widgetData = ref([
{
title: 'Session',
value: '21,459',
change: 29,
desc: 'Total Users',
icon: 'ri-group-line',
iconColor: 'primary',
},
{
title: 'Paid Users',
value: '4,567',
change: 18,
desc: 'Last Week Analytics',
icon: 'ri-user-add-line',
iconColor: 'error',
},
{
title: 'Active Users',
value: '19,860',
change: -14,
desc: 'Last Week Analytics',
icon: 'ri-user-follow-line',
iconColor: 'success',
},
{
title: 'Pending Users',
value: '237',
change: 42,
desc: 'Last Week Analytics',
icon: 'ri-user-search-line',
iconColor: 'warning',
},
])
</script>
<template>
<section>
<!-- 👉 Widgets -->
<div class="d-flex mb-6">
<VRow>
<template
v-for="(data, id) in widgetData"
:key="id"
>
<VCol
cols="12"
md="3"
sm="6"
>
<VCard>
<VCardText>
<div class="d-flex justify-space-between">
<div class="d-flex flex-column gap-y-1">
<span class="text-base text-high-emphasis">{{ data.title }}</span>
<h4 class="text-h4 d-flex align-center gap-2">
{{ data.value }}
<span
class="text-base font-weight-regular"
:class="data.change > 0 ? 'text-success' : 'text-error'"
>({{ prefixWithPlus(data.change) }}%)</span>
</h4>
<p class="text-sm mb-0">
{{ data.desc }}
</p>
</div>
<VAvatar
:color="data.iconColor"
variant="tonal"
rounded
size="42"
>
<VIcon
:icon="data.icon"
size="26"
/>
</VAvatar>
</div>
</VCardText>
</VCard>
</VCol>
</template>
</VRow>
</div>
<VCard
title="Filters"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select Role -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedRole"
label="Select Role"
placeholder="Select Role"
:items="roles"
clearable
clear-icon="ri-close-line"
/>
</VCol>
<!-- 👉 Select Plan -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedPlan"
label="Select Plan"
placeholder="Select Plan"
:items="plans"
clearable
clear-icon="ri-close-line"
/>
</VCol>
<!-- 👉 Select Status -->
<VCol
cols="12"
sm="4"
>
<VSelect
v-model="selectedStatus"
label="Select Status"
placeholder="Select Status"
:items="status"
clearable
clear-icon="ri-close-line"
/>
</VCol>
</VRow>
</VCardText>
<VDivider />
<VCardText class="d-flex flex-wrap gap-4">
<!-- 👉 Export button -->
<VBtn
variant="outlined"
color="secondary"
prepend-icon="ri-upload-2-line"
>
Export
</VBtn>
<VSpacer />
<div class="app-user-search-filter d-flex align-center">
<!-- 👉 Search -->
<VTextField
v-model="searchQuery"
placeholder="Search User"
density="compact"
class="me-4"
/>
<!-- 👉 Add user button -->
<VBtn @click="isAddNewUserDrawerVisible = true">
Add New User
</VBtn>
</div>
</VCardText>
<!-- SECTION datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
: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 align-center">
<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 font-weight-medium user-list-name"
>
{{ item.fullName }}
</RouterLink>
<span class="text-sm text-medium-emphasis">@{{ item.username }}</span>
</div>
</div>
</template>
<!-- Role -->
<template #item.role="{ item }">
<div class="d-flex gap-4">
<VIcon
:icon="resolveUserRoleVariant(item.role).icon"
:color="resolveUserRoleVariant(item.role).color"
/>
<span class="text-capitalize text-high-emphasis">{{ item.role }}</span>
</div>
</template>
<!-- Plan -->
<template #item.plan="{ item }">
<span class="text-capitalize text-high-emphasis">{{ item.currentPlan }}</span>
</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"
color="medium-emphasis"
>
<VIcon
size="24"
icon="ri-more-2-line"
/>
<VMenu activator="parent">
<VList>
<VListItem link>
<template #prepend>
<VIcon icon="ri-download-line" />
</template>
<VListItemTitle>Download</VListItemTitle>
</VListItem>
<VListItem link>
<template #prepend>
<VIcon icon="ri-edit-box-line" />
</template>
<VListItemTitle>Edit</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>
<!-- 👉 Add New User -->
<AddNewUserDrawer
v-model:isDrawerOpen="isAddNewUserDrawerVisible"
@user-data="addNewUser"
/>
</section>
</template>
<style lang="scss">
.app-user-search-filter {
inline-size: 24.0625rem;
}
.text-capitalize {
text-transform: capitalize;
}
.user-list-name:not(:hover) {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
</style>

View File

@@ -0,0 +1,101 @@
<script setup>
import UserBioPanel from '@/views/apps/user/view/UserBioPanel.vue'
import UserTabBillingsPlans from '@/views/apps/user/view/UserTabBillingsPlans.vue'
import UserTabConnections from '@/views/apps/user/view/UserTabConnections.vue'
import UserTabNotifications from '@/views/apps/user/view/UserTabNotifications.vue'
import UserTabOverview from '@/views/apps/user/view/UserTabOverview.vue'
import UserTabSecurity from '@/views/apps/user/view/UserTabSecurity.vue'
const route = useRoute('apps-user-view-id')
const userTab = ref(null)
const tabs = [
{
icon: 'ri-group-line',
title: 'Overview',
},
{
icon: 'ri-lock-2-line',
title: 'Security',
},
{
icon: 'ri-bookmark-line',
title: 'Billing & Plan',
},
{
icon: 'ri-notification-4-line',
title: 'Notifications',
},
{
icon: 'ri-link-m',
title: 'Connections',
},
]
const { data: userData } = await useApi(`/apps/users/${ route.params.id }`)
</script>
<template>
<VRow v-if="userData">
<VCol
cols="12"
md="5"
lg="4"
>
<UserBioPanel :user-data="userData" />
</VCol>
<VCol
cols="12"
md="7"
lg="8"
>
<VTabs
v-model="userTab"
class="v-tabs-pill"
>
<VTab
v-for="tab in tabs"
:key="tab.icon"
>
<VIcon
start
:icon="tab.icon"
/>
<span>{{ tab.title }}</span>
</VTab>
</VTabs>
<VWindow
v-model="userTab"
class="mt-6 disable-tab-transition"
:touch="false"
>
<VWindowItem>
<UserTabOverview />
</VWindowItem>
<VWindowItem>
<UserTabSecurity />
</VWindowItem>
<VWindowItem>
<UserTabBillingsPlans />
</VWindowItem>
<VWindowItem>
<UserTabNotifications />
</VWindowItem>
<VWindowItem>
<UserTabConnections />
</VWindowItem>
</VWindow>
</VCol>
</VRow>
<VCard v-else>
<VCardTitle class="text-center">
No User Found
</VCardTitle>
</VCard>
</template>