first commit
This commit is contained in:
308
resources/js/pages/apps/academy/course-details.vue
Normal file
308
resources/js/pages/apps/academy/course-details.vue
Normal 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>
|
356
resources/js/pages/apps/academy/dashboard.vue
Normal file
356
resources/js/pages/apps/academy/dashboard.vue
Normal 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>
|
229
resources/js/pages/apps/academy/my-course.vue
Normal file
229
resources/js/pages/apps/academy/my-course.vue
Normal 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>
|
175
resources/js/pages/apps/calendar.vue
Normal file
175
resources/js/pages/apps/calendar.vue
Normal 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>
|
429
resources/js/pages/apps/chat.vue
Normal file
429
resources/js/pages/apps/chat.vue
Normal 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>
|
107
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal file
107
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal 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>
|
187
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal file
187
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal 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>
|
587
resources/js/pages/apps/ecommerce/manage-review.vue
Normal file
587
resources/js/pages/apps/ecommerce/manage-review.vue
Normal 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>
|
413
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal file
413
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal 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>
|
395
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal file
395
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal 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>
|
688
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal file
688
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal 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>
|
310
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal file
310
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal 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>
|
518
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal file
518
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal 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>
|
384
resources/js/pages/apps/ecommerce/referrals.vue
Normal file
384
resources/js/pages/apps/ecommerce/referrals.vue
Normal 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>
|
99
resources/js/pages/apps/ecommerce/settings.vue
Normal file
99
resources/js/pages/apps/ecommerce/settings.vue
Normal 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>
|
511
resources/js/pages/apps/email/index.vue
Normal file
511
resources/js/pages/apps/email/index.vue
Normal 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>
|
170
resources/js/pages/apps/invoice/add/index.vue
Normal file
170
resources/js/pages/apps/invoice/add/index.vue
Normal 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>
|
166
resources/js/pages/apps/invoice/edit/[id].vue
Normal file
166
resources/js/pages/apps/invoice/edit/[id].vue
Normal 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>
|
454
resources/js/pages/apps/invoice/list/index.vue
Normal file
454
resources/js/pages/apps/invoice/list/index.vue
Normal 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>
|
438
resources/js/pages/apps/invoice/preview/[id].vue
Normal file
438
resources/js/pages/apps/invoice/preview/[id].vue
Normal 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>
|
55
resources/js/pages/apps/logistics/dashboard.vue
Normal file
55
resources/js/pages/apps/logistics/dashboard.vue
Normal 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>
|
378
resources/js/pages/apps/logistics/fleet.vue
Normal file
378
resources/js/pages/apps/logistics/fleet.vue
Normal 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>
|
203
resources/js/pages/apps/permissions/index.vue
Normal file
203
resources/js/pages/apps/permissions/index.vue
Normal 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>
|
34
resources/js/pages/apps/roles/index.vue
Normal file
34
resources/js/pages/apps/roles/index.vue
Normal 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>
|
533
resources/js/pages/apps/user/list/index.vue
Normal file
533
resources/js/pages/apps/user/list/index.vue
Normal 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>
|
101
resources/js/pages/apps/user/view/[id].vue
Normal file
101
resources/js/pages/apps/user/view/[id].vue
Normal 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>
|
Reference in New Issue
Block a user