Initial commit
This commit is contained in:
315
resources/js/pages/apps/academy/course-details.vue
Normal file
315
resources/js/pages/apps/academy/course-details.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<script setup>
|
||||
import { VideoPlayer } from '@videojs-player/vue'
|
||||
import InstructorPoster from '@images/pages/instructor-poster.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?.instructor }}</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="24"
|
||||
class="cursor-pointer"
|
||||
icon="tabler-share"
|
||||
/>
|
||||
<VIcon
|
||||
size="24"
|
||||
class="cursor-pointer"
|
||||
icon="tabler-bookmarks"
|
||||
/>
|
||||
</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="InstructorPoster"
|
||||
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="tabler-check"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Skill Level: {{ courseDetails?.skillLevel }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-users"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Students: {{ courseDetails?.totalStudents }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-world"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Languages: {{ courseDetails?.language }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-file"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Captions: {{ courseDetails?.isCaptions }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VList class="card-list text-medium-emphasis">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-video"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Lectures: {{ courseDetails?.totalLectures }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
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 gap-x-4">
|
||||
<VAvatar
|
||||
:image="courseDetails?.instructorAvatar"
|
||||
size="38"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ courseDetails?.instructor }}
|
||||
</h6>
|
||||
<div class="text-body-2">
|
||||
{{ 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"
|
||||
class="expansion-panels-width-border"
|
||||
>
|
||||
<template
|
||||
v-for="(section, index) in courseDetails?.content"
|
||||
:key="index"
|
||||
>
|
||||
<VExpansionPanel
|
||||
elevation="0"
|
||||
:value="index"
|
||||
expand-icon="tabler-chevron-right"
|
||||
collapse-icon="tabler-chevron-down"
|
||||
>
|
||||
<template #title>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-1">
|
||||
{{ section.title }}
|
||||
</h5>
|
||||
<div class="text-medium-emphasis font-weight-normal">
|
||||
{{ 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
|
||||
:model-value="topic.isCompleted"
|
||||
class="me-1"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="text-high-emphasis font-weight-medium">
|
||||
{{ id + 1 }} . {{ topic.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
<div class="text-body-2">
|
||||
{{ topic.time }}
|
||||
</div>
|
||||
</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</template>
|
||||
</VExpansionPanel>
|
||||
</template>
|
||||
</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">
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
body .v-layout .v-application__wrap {
|
||||
.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) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.v-expansion-panel-title__overlay {
|
||||
opacity: var(--v-hover-opacity) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
.v-icon {
|
||||
block-size: 1.5rem !important;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 1.5rem !important;
|
||||
inline-size: 1.5rem !important;
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-expansion-panel-text {
|
||||
&__wrapper {
|
||||
padding-block: 1rem;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-list {
|
||||
.v-list-item__prepend {
|
||||
.v-list-item__spacer {
|
||||
inline-size: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
253
resources/js/pages/apps/academy/dashboard.vue
Normal file
253
resources/js/pages/apps/academy/dashboard.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<script setup>
|
||||
import AcademyAssignmentProgress from '@/views/apps/academy/AcademyAssignmentProgress.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 AcademyTopicYouAreInterested from '@/views/apps/academy/AcademyTopicYouAreInterested.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 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: '15px',
|
||||
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-6">
|
||||
<!-- 👉 Welcome -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
:class="$vuetify.display.mdAndUp ? 'border-e' : 'border-b'"
|
||||
>
|
||||
<div class="pe-3">
|
||||
<h5 class="text-h5 mb-2">
|
||||
Welcome back,<span class="text-h4"> Felecia 👋🏻 </span>
|
||||
</h5>
|
||||
|
||||
<div
|
||||
class="text-wrap text-body-1"
|
||||
style="max-inline-size: 360px;"
|
||||
>
|
||||
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-4 flex-column flex-md-row mt-4">
|
||||
<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 align-center">
|
||||
<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"
|
||||
:class="`text-${color}`"
|
||||
>
|
||||
{{ value }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Time Spending -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<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="text-body-1 mb-7">
|
||||
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"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
+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">
|
||||
<!-- 👉 Topics you are interested in -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<!-- 👉 Topic You are Interested in -->
|
||||
<AcademyTopicYouAreInterested />
|
||||
</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";
|
||||
</style>
|
||||
234
resources/js/pages/apps/academy/my-course.vue
Normal file
234
resources/js/pages/apps/academy/my-course.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup>
|
||||
import { VideoPlayer } from '@videojs-player/vue'
|
||||
import AcademyMyCourses from '@/views/apps/academy/AcademyMyCourses.vue'
|
||||
import boyAppAcademy from '@images/illustrations/boy-app-academy.png'
|
||||
import girlAppAcademy from '@images/illustrations/girl-app-academy.png'
|
||||
import academyCourseIllustration1 from '@images/pages/academy-course-illustration1.png'
|
||||
import academyCourseIllustration2Dark from '@images/pages/academy-course-illustration2-dark.png'
|
||||
import academyCourseIllustration2Light from '@images/pages/academy-course-illustration2-light.png'
|
||||
import guitarCoursePoster from '@images/pages/guitar-course-poster.png'
|
||||
import singingCoursePoster from '@images/pages/singing-course-poster.png'
|
||||
|
||||
const academyCourseIllustration2 = useGenerateImageVariant(academyCourseIllustration2Light, academyCourseIllustration2Dark)
|
||||
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' : $vuetify.display.xs ? 'w-100' : 'w-75'"
|
||||
>
|
||||
<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 text-no-wrap"> 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-4 flex-wrap">
|
||||
<div
|
||||
class="flex-grow-1"
|
||||
style="max-inline-size: 350px;"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Find your course"
|
||||
/>
|
||||
</div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
icon="tabler-search"
|
||||
class="rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
:src="academyCourseIllustration1"
|
||||
class="illustration1 d-none d-md-block flip-in-rtl"
|
||||
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
|
||||
v-for="{ title, btnText, color, description, image } in [
|
||||
{ title: 'Earn a Certificate', description: 'Get the right professional certificate program for you.', btnText: 'View Programs', color: 'primary', image: boyAppAcademy },
|
||||
{ title: 'Best Rated Courses', description: 'Enroll now in the most popular and best rated courses.', btnText: 'View Courses', color: 'error', image: girlAppAcademy },
|
||||
]"
|
||||
:key="title"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard
|
||||
|
||||
flat
|
||||
:color="`rgba(var(--v-theme-${color}), var(--v-selected-opacity))`"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between gap-4 flex-column-reverse flex-sm-row">
|
||||
<div class="text-center text-sm-start">
|
||||
<h5
|
||||
class="text-h5 mb-1"
|
||||
:class="`text-${color}`"
|
||||
>
|
||||
<div class="d-flex justify-space-between gap-4 flex-column-reverse flex-sm-row">
|
||||
<div class="text-center text-sm-start">
|
||||
<h5
|
||||
class="text-h5 mb-1"
|
||||
:class="`text-${color}`"
|
||||
>
|
||||
{{ title }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<p
|
||||
class="text-body-1 mx-auto"
|
||||
style="max-inline-size: 300px;"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
<VBtn :color="color">
|
||||
{{ btnText }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="align-self-center">
|
||||
<div class="align-self-center">
|
||||
<img
|
||||
:src="image"
|
||||
height="127"
|
||||
class="flip-in-rtl"
|
||||
>
|
||||
</div>
|
||||
</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="tabler-gift"
|
||||
size="36"
|
||||
/>
|
||||
</VAvatar>
|
||||
<h4 class="text-h4 font-weight-medium">
|
||||
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="px-2 pt-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>
|
||||
<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="px-2 pt-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>
|
||||
<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>
|
||||
190
resources/js/pages/apps/calendar.vue
Normal file
190
resources/js/pages/apps/calendar.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<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 = []
|
||||
},
|
||||
})
|
||||
|
||||
const jumpToDateFn = date => {
|
||||
jumpToDate(date)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<!-- `z-index: 0` Allows overlapping vertical nav on calendar -->
|
||||
<VLayout style="z-index: 0;">
|
||||
<!-- 👉 Navigation drawer -->
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
data-allow-mismatch
|
||||
width="292"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
class="calendar-add-event-drawer"
|
||||
:temporary="$vuetify.display.mdAndDown"
|
||||
>
|
||||
<div style="margin: 1.5rem;">
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isEventHandlerSidebarActive = true"
|
||||
>
|
||||
Add event
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex align-center justify-center pa-2">
|
||||
<AppDateTimePicker
|
||||
id="calendar-date-picker"
|
||||
:model-value="new Date().toJSON().slice(0, 10)"
|
||||
:config="{ inline: true }"
|
||||
class="calendar-date-picker"
|
||||
@update:model-value="jumpToDateFn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<div class="pa-6">
|
||||
<h6 class="text-lg font-weight-medium mb-4">
|
||||
Event Filters
|
||||
</h6>
|
||||
|
||||
<div class="d-flex flex-column calendars-checkbox">
|
||||
<VCheckbox
|
||||
id="check-all-events"
|
||||
v-model="checkAll"
|
||||
label="View all"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-for="(calendar, index) in store.availableCalendars"
|
||||
:id="`${index}`"
|
||||
: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:is-drawer-open="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;
|
||||
}
|
||||
|
||||
&.v-navigation-drawer--temporary:not(.v-navigation-drawer--active) {
|
||||
transform: translateX(-110%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1279px) {
|
||||
.calendar-add-event-drawer {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-layout {
|
||||
overflow: visible !important;
|
||||
|
||||
.v-card {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1576
resources/js/pages/apps/chat.vue
Normal file
1576
resources/js/pages/apps/chat.vue
Normal file
File diff suppressed because it is too large
Load Diff
192
resources/js/pages/apps/components/ChatInput.vue
Normal file
192
resources/js/pages/apps/components/ChatInput.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
msg: String,
|
||||
isInputDisabled: Boolean,
|
||||
activeModelColor: String,
|
||||
selectedAttachments: Array,
|
||||
showSuggestions: Boolean,
|
||||
suggestions: Array,
|
||||
selectedSuggestionIndex: Number,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:msg',
|
||||
'send',
|
||||
'compositionstart',
|
||||
'compositionend',
|
||||
'keydown',
|
||||
'remove-attachment',
|
||||
'select-suggestion',
|
||||
]);
|
||||
|
||||
const inputRef = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<div
|
||||
class="chat-input-container rounded-3xl pa-3 mb-2"
|
||||
:class="{ 'pt-0': selectedAttachments.length === 0 }"
|
||||
>
|
||||
<div
|
||||
v-if="selectedAttachments.length > 0"
|
||||
class="d-flex flex-wrap gap-2 mt-3 mb-2 attachment-chip-container"
|
||||
>
|
||||
<VChip
|
||||
v-for="(file, index) in selectedAttachments"
|
||||
:key="index"
|
||||
closable
|
||||
@click:close="emit('remove-attachment', index)"
|
||||
color="grey-lighten-3"
|
||||
class="align-center attachment-chip"
|
||||
>
|
||||
<VIcon start :icon="'tabler-file'" size="small"></VIcon>
|
||||
<span class="attachment-name">{{ file.name }}</span>
|
||||
<span class="attachment-size">({{ (file.size / (1024 * 1024)).toFixed(1) }} MB)</span>
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div class="position-relative">
|
||||
<VTextarea
|
||||
ref="inputRef"
|
||||
:model-value="msg"
|
||||
auto-grow
|
||||
rows="1"
|
||||
row-height="20"
|
||||
:disabled="isInputDisabled"
|
||||
hide-details
|
||||
variant="plain"
|
||||
persistent-placeholder
|
||||
density="comfortable"
|
||||
class="chat-textarea"
|
||||
placeholder="Type your message..."
|
||||
no-resize
|
||||
@update:model-value="emit('update:msg', $event)"
|
||||
@keydown="emit('keydown', $event)"
|
||||
@compositionstart="emit('compositionstart')"
|
||||
@compositionend="emit('compositionend')"
|
||||
></VTextarea>
|
||||
|
||||
<div v-if="showSuggestions" class="suggestions-dropdown rounded pa-2">
|
||||
<div class="suggestions-header text-caption text-grey-darken-1 mb-1 px-2">Suggestions</div>
|
||||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
class="suggestion-item pa-2 rounded d-flex align-center"
|
||||
:class="{ 'suggestion-selected': selectedSuggestionIndex === index }"
|
||||
@click="emit('select-suggestion', suggestion)"
|
||||
@mouseenter="emit('update:selectedSuggestionIndex', index)"
|
||||
>
|
||||
<VIcon icon="tabler-search" size="16" class="me-2 text-grey-darken-1" />
|
||||
<span class="suggestion-text">{{ suggestion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center justify-end mt-1">
|
||||
<VBtn
|
||||
icon
|
||||
size="large"
|
||||
:color="activeModelColor"
|
||||
@click="emit('send')"
|
||||
:disabled="isInputDisabled || !msg.trim()"
|
||||
>
|
||||
<VIcon icon="tabler-send" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.attachment-chip-container {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
margin-right: -8px;
|
||||
margin-left: -8px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.attachment-chip-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.attachment-chip {
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.attachment-size {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.suggestions-dropdown {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 16px rgba(var(--v-theme-on-surface), 0.15);
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
z-index: 15;
|
||||
margin-bottom: 8px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
animation: suggestions-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.suggestions-header {
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.suggestion-item:hover,
|
||||
.suggestion-item.suggestion-selected {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@keyframes suggestions-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
resources/js/pages/apps/components/FormQuestion.vue
Normal file
54
resources/js/pages/apps/components/FormQuestion.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
form: Object,
|
||||
activeModelColor: String,
|
||||
});
|
||||
const emit = defineEmits(["select"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<div class="form-title font-weight-bold mb-2">
|
||||
{{ form.title }}
|
||||
</div>
|
||||
<div class="form-question mb-3">
|
||||
{{ form.question }}
|
||||
</div>
|
||||
<div class="form-options d-flex gap-2">
|
||||
<VBtn
|
||||
v-for="(option, optIndex) in form.options"
|
||||
:key="optIndex"
|
||||
:color="activeModelColor"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="form-option-btn"
|
||||
@click="emit('select', option)"
|
||||
>
|
||||
{{ option }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-title {
|
||||
font-size: 1.1rem;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.form-question {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-options .form-option-btn {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.form-options .form-option-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
107
resources/js/pages/apps/components/MessageItem.vue
Normal file
107
resources/js/pages/apps/components/MessageItem.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { themes } from '@/plugins/vuetify/theme';
|
||||
|
||||
const props = defineProps({
|
||||
message: Object,
|
||||
activeModelColor: String,
|
||||
activeModelBgColor: String,
|
||||
models: Array,
|
||||
selectedModelIdentifier: String,
|
||||
});
|
||||
|
||||
const getFileIcon = (fileType) => {
|
||||
if (fileType.startsWith('image/')) return 'tabler-photo';
|
||||
if (fileType.includes('pdf')) return 'tabler-file-type-pdf';
|
||||
if (fileType.includes('word')) return 'tabler-file-type-doc';
|
||||
if (fileType.includes('text')) return 'tabler-file-text';
|
||||
return 'tabler-file';
|
||||
};
|
||||
|
||||
const getFileSize = (size) => {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="my-3 d-flex"
|
||||
:class="[
|
||||
message.sender === 'user' ? 'justify-end' : 'justify-start',
|
||||
message.isAnimating ? 'is-animating' : '',
|
||||
message.sender === 'user' ? 'user-message' : 'bot-message',
|
||||
message.isSystemMessage ? 'system-message' : '',
|
||||
]"
|
||||
>
|
||||
<template v-if="message.sender !== 'user'">
|
||||
<VAvatar
|
||||
size="32"
|
||||
:color="message.isSystemMessage ? 'grey' : activeModelColor"
|
||||
variant="tonal"
|
||||
class="me-2 message-avatar"
|
||||
>
|
||||
<VIcon
|
||||
:icon="message.isSystemMessage ? 'tabler-info-circle' : 'tabler-robot'"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="pa-3 rounded-lg message-bubble"
|
||||
:class="[
|
||||
message.sender === 'user'
|
||||
? 'bg-primary text-white'
|
||||
: message.isSystemMessage
|
||||
? 'bg-grey-lighten-3 text-grey-darken-3'
|
||||
: `bg-${activeModelBgColor} text-white`,
|
||||
message.isAnimating ? 'animate-message' : '',
|
||||
]"
|
||||
>
|
||||
<div v-if="!message.form && !message.multiForm && !message.type">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
|
||||
<div v-if="message.isAttachment && message.files" class="attachment-preview mt-2">
|
||||
<div
|
||||
v-for="(file, fileIndex) in message.files"
|
||||
:key="fileIndex"
|
||||
class="d-flex align-center mb-1"
|
||||
>
|
||||
<VIcon :icon="getFileIcon(file.type)" size="small" class="me-1"></VIcon>
|
||||
<span class="text-caption">{{ file.name }} ({{ getFileSize(file.size) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="message.sender === 'user'">
|
||||
<VAvatar size="32" color="grey" variant="tonal" class="ms-2 message-avatar">
|
||||
<VIcon icon="tabler-user" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-avatar {
|
||||
opacity: 0;
|
||||
animation: fade-in 0.3s ease forwards;
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
63
resources/js/pages/apps/components/ModelSelector.vue
Normal file
63
resources/js/pages/apps/components/ModelSelector.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
models: Array,
|
||||
selectedModelIdentifier: String,
|
||||
activeModelColor: String,
|
||||
showModelMenu: Boolean,
|
||||
});
|
||||
const emit = defineEmits(['update:model', 'update:menu']);
|
||||
|
||||
const selectedModelName = computed(() => {
|
||||
const model = props.models.find((m) => m.identifier === props.selectedModelIdentifier);
|
||||
return model ? model.name : 'Select Model';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VMenu :model-value="showModelMenu" location="top" @update:model-value="emit('update:menu', $event)">
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<VBtn
|
||||
v-bind="menuProps"
|
||||
:color="activeModelColor"
|
||||
variant="text"
|
||||
class="text-none model-select-btn"
|
||||
density="comfortable"
|
||||
>
|
||||
<span class="d-none d-sm-block">{{ selectedModelName }}</span>
|
||||
<span class="d-block d-sm-none">Model</span>
|
||||
<VIcon icon="tabler-chevron-down" class="ms-1" />
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<VList density="compact" max-height="300">
|
||||
<VListItem
|
||||
v-for="model in models"
|
||||
:key="model.identifier"
|
||||
:value="model.identifier"
|
||||
@click="emit('update:model', model.identifier); emit('update:menu', false)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<div
|
||||
class="model-color-indicator"
|
||||
:style="`background-color: var(--v-theme-${model.color})`"
|
||||
></div>
|
||||
</template>
|
||||
<VListItemTitle>{{ model.name }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.model-select-btn {
|
||||
min-width: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.model-color-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
99
resources/js/pages/apps/components/MultiStepForm.vue
Normal file
99
resources/js/pages/apps/components/MultiStepForm.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
step: Object,
|
||||
stepNumber: Number,
|
||||
totalSteps: Number,
|
||||
activeModelColor: String,
|
||||
activeModelBgColor: String,
|
||||
});
|
||||
const emit = defineEmits(["cancel", "select"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-form-container">
|
||||
<div class="multi-form-header mb-3">
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<div class="form-title font-weight-bold">
|
||||
{{ step.formTitle }}
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VChip :color="activeModelColor" size="small" variant="tonal">
|
||||
{{ stepNumber }}/{{ totalSteps }}
|
||||
</VChip>
|
||||
<VBtn icon size="x-small" @click="emit('cancel')" class="ms-2">
|
||||
<VIcon icon="tabler-x" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VProgressLinear
|
||||
:model-value="(stepNumber / totalSteps) * 100"
|
||||
:color="activeModelColor"
|
||||
height="4"
|
||||
rounded
|
||||
class="mb-3"
|
||||
></VProgressLinear>
|
||||
</div>
|
||||
|
||||
<div class="step-title font-weight-medium mb-2">
|
||||
{{ step.title }}
|
||||
</div>
|
||||
|
||||
<div class="form-question mb-3">
|
||||
{{ step.question }}
|
||||
</div>
|
||||
|
||||
<div v-if="step.type === 'options'" class="form-options">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VBtn
|
||||
v-for="(option, optIndex) in step.options"
|
||||
:key="optIndex"
|
||||
:color="activeModelColor"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="form-option-btn"
|
||||
@click="emit('select', { id: step.id, value: option })"
|
||||
>
|
||||
{{ option }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.multi-form-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.multi-form-header {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 1.1rem;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
}
|
||||
|
||||
.form-question {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-options .form-option-btn {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.form-options .form-option-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
48
resources/js/pages/apps/components/QuickReplies.vue
Normal file
48
resources/js/pages/apps/components/QuickReplies.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
replies: Array,
|
||||
activeModelColor: String,
|
||||
isInputCentered: Boolean,
|
||||
});
|
||||
const emit = defineEmits(['select']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quick-replies-container">
|
||||
<div
|
||||
v-for="(reply, index) in replies"
|
||||
:key="index"
|
||||
class="quick-reply-item"
|
||||
>
|
||||
<VBtn
|
||||
:variant="isInputCentered ? 'tonal' : 'outlined'"
|
||||
:color="activeModelColor"
|
||||
size="small"
|
||||
@click="emit('select', reply)"
|
||||
class="w-100"
|
||||
>
|
||||
{{ reply }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.quick-replies-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.quick-replies-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-reply-item {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
113
resources/js/pages/apps/components/SuggestionsDropdown.vue
Normal file
113
resources/js/pages/apps/components/SuggestionsDropdown.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
suggestions: Array,
|
||||
selectedIndex: Number,
|
||||
activeModelColor: String,
|
||||
});
|
||||
const emit = defineEmits(["select", "hover"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="suggestions-dropdown rounded pa-2">
|
||||
<div class="suggestions-header text-caption text-grey-darken-1 mb-1 px-2">
|
||||
Suggestions
|
||||
</div>
|
||||
<div
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="index"
|
||||
class="suggestion-item pa-2 rounded d-flex align-center"
|
||||
:class="{ 'suggestion-selected': selectedIndex === index }"
|
||||
@click="emit('select', suggestion)"
|
||||
@mouseenter="emit('hover', index)"
|
||||
>
|
||||
<VIcon icon="tabler-search" size="16" class="me-2 text-grey-darken-1" />
|
||||
<span class="suggestion-text">{{ suggestion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.suggestions-dropdown {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 16px rgba(var(--v-theme-on-surface), 0.15);
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
z-index: 15;
|
||||
margin-bottom: 8px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
animation: suggestions-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
.suggestions-header {
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.suggestion-item:hover,
|
||||
.suggestion-item.suggestion-selected {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@keyframes suggestions-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.suggestions-dropdown {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions-dropdown::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.suggestions-dropdown::-webkit-scrollbar-track {
|
||||
background: rgba(var(--v-theme-on-surface), 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.suggestions-dropdown::-webkit-scrollbar-thumb {
|
||||
background: rgba(var(--v-theme-on-surface), 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.suggestions-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(var(--v-theme-on-surface), 0.3);
|
||||
}
|
||||
</style>
|
||||
72
resources/js/pages/apps/components/TypingIndicator.vue
Normal file
72
resources/js/pages/apps/components/TypingIndicator.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
activeModelColor: String,
|
||||
activeModelBgColor: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="my-3 d-flex justify-start typing-indicator-container">
|
||||
<VAvatar
|
||||
size="32"
|
||||
:color="activeModelColor"
|
||||
variant="tonal"
|
||||
class="me-2 message-avatar"
|
||||
>
|
||||
<VIcon icon="tabler-robot" />
|
||||
</VAvatar>
|
||||
|
||||
<div
|
||||
class="pa-3 rounded-lg message-bubble"
|
||||
:class="`bg-${activeModelBgColor} text-${activeModelColor}`"
|
||||
>
|
||||
<div class="bot-typing-indicator">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.typing-indicator-container {
|
||||
animation: fade-in 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.bot-typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bot-typing-indicator .dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
margin: 0 2px;
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.bot-typing-indicator .dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.bot-typing-indicator .dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(0.7);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
resources/js/pages/apps/components/UploadModal.vue
Normal file
78
resources/js/pages/apps/components/UploadModal.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
files: Array,
|
||||
fileError: String,
|
||||
activeModelColor: String,
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "triggerInput", "remove", "done"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog :model-value="modelValue" max-width="500" @update:modelValue="emit('update:modelValue', $event)">
|
||||
<VCard>
|
||||
<VCardTitle class="pb-2 pt-4">
|
||||
<span class="text-h5">Upload Files</span>
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<div class="text-subtitle-2 mb-4">
|
||||
Select files to upload (Max 5 files, 5MB each)
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="upload-area pa-6 border-dashed rounded d-flex flex-column align-center justify-center"
|
||||
@click="emit('triggerInput')"
|
||||
@dragover.prevent
|
||||
@drop.prevent="$event => emit('triggerInput', $event)"
|
||||
>
|
||||
<VIcon :icon="'tabler-upload'" :size="36" :color="activeModelColor" class="mb-3" />
|
||||
<div class="text-body-1 mb-1">Drag files here or click to upload</div>
|
||||
<div class="text-caption text-grey">Supported formats: Images, PDF, DOC, DOCX, TXT</div>
|
||||
</div>
|
||||
|
||||
<div v-if="fileError" class="text-error mt-2">{{ fileError }}</div>
|
||||
|
||||
<div v-if="files.length > 0">
|
||||
<div class="text-subtitle-2 mt-4 mb-2">Selected files:</div>
|
||||
<VList density="compact" class="bg-grey-lighten-5 rounded">
|
||||
<VListItem v-for="(file, index) in files" :key="index">
|
||||
<template v-slot:prepend>
|
||||
<VIcon :icon="file.type.startsWith('image/') ? 'tabler-photo' : file.type.includes('pdf') ? 'tabler-file-type-pdf' : file.type.includes('word') ? 'tabler-file-type-doc' : file.type.includes('text') ? 'tabler-file-text' : 'tabler-file'" />
|
||||
</template>
|
||||
<VListItemTitle>{{ file.name }}</VListItemTitle>
|
||||
<VListItemSubtitle>{{ (file.size / (1024 * 1024)).toFixed(1) }} MB</VListItemSubtitle>
|
||||
<template v-slot:append>
|
||||
<VBtn icon size="small" variant="text" @click="emit('remove', index)">
|
||||
<VIcon icon="tabler-x" />
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="pb-4 px-4">
|
||||
<VSpacer></VSpacer>
|
||||
<VBtn color="grey-darken-1" variant="text" @click="emit('update:modelValue', false)">Cancel</VBtn>
|
||||
<VBtn :color="activeModelColor" variant="tonal" @click="emit('done')" :disabled="files.length === 0">Done</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.border-dashed {
|
||||
border: 2px dashed rgba(var(--v-theme-on-surface), 0.2);
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.05);
|
||||
border-color: rgba(var(--v-theme-on-surface), 0.4);
|
||||
}
|
||||
</style>
|
||||
120
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal file
120
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup>
|
||||
import ECommerceAddCustomerDrawer from '@/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue'
|
||||
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 = [
|
||||
{
|
||||
title: 'Overview',
|
||||
icon: 'tabler-user',
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
icon: 'tabler-lock',
|
||||
},
|
||||
{
|
||||
title: 'Address & Billing',
|
||||
icon: 'tabler-map-pin',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
icon: 'tabler-bell',
|
||||
},
|
||||
]
|
||||
|
||||
const { data } = await useApi(`/apps/ecommerce/customers/${ route.params.id }`)
|
||||
if (data.value)
|
||||
customerData.value = data.value
|
||||
const isAddCustomerDrawerOpen = ref(false)
|
||||
</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 mb-1">
|
||||
Customer ID #{{ route.params.id }}
|
||||
</h4>
|
||||
<div class="text-body-1">
|
||||
Aug 17, 2020, 5:48 (ET)
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-4">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="error"
|
||||
>
|
||||
Delete Customer
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 👉 Customer Profile -->
|
||||
<VRow v-if="customerData">
|
||||
<VCol
|
||||
v-if="customerData"
|
||||
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-3 disable-tab-transition"
|
||||
>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.title"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
start
|
||||
:icon="tab.icon"
|
||||
/>
|
||||
{{ tab.title }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="userTab"
|
||||
class="disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem>
|
||||
<CustomerTabOverview />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabSecurity />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabAddressAndBilling />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabNotification />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<div v-else>
|
||||
<VAlert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
Invoice with ID {{ route.params.id }} not found!
|
||||
</VAlert>
|
||||
</div>
|
||||
<ECommerceAddCustomerDrawer v-model:is-drawer-open="isAddCustomerDrawerOpen" />
|
||||
</div>
|
||||
</template>
|
||||
173
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal file
173
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<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 => {
|
||||
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">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
style="max-inline-size: 280px; min-inline-size: 280px;"
|
||||
placeholder="Search Name"
|
||||
/>
|
||||
<div class="d-flex flex-row gap-4 align-center flex-wrap">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[5, 10, 20, 50, 100]"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
prepend-icon="tabler-upload"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isAddCustomerDrawerOpen = !isAddCustomerDrawerOpen"
|
||||
>
|
||||
Add Customer
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
<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"
|
||||
:variant="!item.avatar ? 'tonal' : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.customer) }}</span>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: item.customerId } }"
|
||||
class="text-link font-weight-medium d-inline-block"
|
||||
style="line-height: 1.375rem;"
|
||||
>
|
||||
{{ item.customer }}
|
||||
</RouterLink>
|
||||
<div class="text-body-2">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.customerId="{ item }">
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
#{{ item.customerId }}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalCustomers"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
|
||||
<ECommerceAddCustomerDrawer v-model:is-drawer-open="isAddCustomerDrawerOpen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-title:hover {
|
||||
color: rgba(var(--v-theme-primary)) !important;
|
||||
}
|
||||
</style>
|
||||
573
resources/js/pages/apps/ecommerce/manage-review.vue
Normal file
573
resources/js/pages/apps/ecommerce/manage-review.vue
Normal file
@@ -0,0 +1,573 @@
|
||||
<script setup>
|
||||
const selectedStatus = ref('All')
|
||||
const searchQuery = ref('')
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
const selectedRows = 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 => {
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const deleteReview = async id => {
|
||||
await $api(`/apps/ecommerce/reviews/${ id }`, { method: 'DELETE' })
|
||||
|
||||
const index = selectedRows.value.findIndex(row => row === id)
|
||||
if (index !== -1)
|
||||
selectedRows.value.splice(index, 1)
|
||||
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 config = {
|
||||
colorsLabel: { success: '#28c76f29' },
|
||||
colors: { success: '#28c76f' },
|
||||
}
|
||||
|
||||
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: [
|
||||
config.colorsLabel.success,
|
||||
config.colorsLabel.success,
|
||||
config.colorsLabel.success,
|
||||
config.colorsLabel.success,
|
||||
config.colors.success,
|
||||
config.colorsLabel.success,
|
||||
config.colorsLabel.success,
|
||||
],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
barHeight: '75%',
|
||||
columnWidth: '25%',
|
||||
borderRadius: 4,
|
||||
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: 4,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1400,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 4,
|
||||
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: 600,
|
||||
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: 4,
|
||||
columnWidth: '30%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<!-- 👉 Total Review Card -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex gap-6 flex-column flex-sm-row">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<h3 class="text-h3 text-primary">
|
||||
4.89
|
||||
</h3>
|
||||
<VIcon
|
||||
icon="tabler-star-filled"
|
||||
color="primary"
|
||||
size="32"
|
||||
/>
|
||||
</div>
|
||||
<h6 class="my-2 text-h6">
|
||||
Total 187 reviews
|
||||
</h6>
|
||||
<div class="mb-2 text-wrap">
|
||||
All reviews are from genuine customers
|
||||
</div>
|
||||
<VChip
|
||||
color="primary"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
+5 This week
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<VDivider :vertical="$vuetify.display.smAndUp" />
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div
|
||||
v-for="(review, index) in reviewCardData"
|
||||
:key="index"
|
||||
class="d-flex align-center gap-x-4"
|
||||
:class="index !== reviewCardData.length - 1 ? 'mb-3' : ''"
|
||||
>
|
||||
<div class="text-no-wrap text-sm">
|
||||
{{ review.rating }} Star
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-grow-1"
|
||||
style="min-inline-size: 150px;"
|
||||
>
|
||||
<VProgressLinear
|
||||
color="primary"
|
||||
height="8"
|
||||
:model-value="(review.value / 185) * 100"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
{{ review.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between flex-sm-row flex-column">
|
||||
<div>
|
||||
<h5 class="text-h5 mb-2">
|
||||
Reviews statistics
|
||||
</h5>
|
||||
<div class="mb-8 mb-sm-12">
|
||||
<div class="d-inline-block me-2">
|
||||
12 New Reviews
|
||||
</div>
|
||||
<VChip
|
||||
color="success"
|
||||
size="small"
|
||||
label
|
||||
>
|
||||
+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>
|
||||
<div>
|
||||
<VueApexCharts
|
||||
id="shipment-statistics"
|
||||
type="bar"
|
||||
height="152"
|
||||
:options="reviewStatChartConfig"
|
||||
:series="reviewStatChartSeries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between flex-wrap gap-6 ">
|
||||
<div>
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
style="max-inline-size: 200px; min-inline-size: 200px;"
|
||||
placeholder="Search Review"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-row gap-4 align-center flex-wrap">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[10, 25, 50, 100]"
|
||||
style="inline-size: 6.25rem;"
|
||||
/>
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
style="max-inline-size: 7.5rem;min-inline-size: 7.5rem;"
|
||||
:items="[
|
||||
{ title: 'All', value: 'All' },
|
||||
{ title: 'Published', value: 'Published' },
|
||||
{ title: 'Pending', value: 'Pending' },
|
||||
]"
|
||||
/>
|
||||
<VBtn
|
||||
prepend-icon="tabler-upload"
|
||||
variant="tonal"
|
||||
color="default"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:model-value="selectedRows"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="reviews"
|
||||
show-select
|
||||
:items-length="totalReviews"
|
||||
class="text-no-wrap"
|
||||
@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>
|
||||
<div class="text-body-2 text-wrap clamp-text">
|
||||
{{ item.companyName }}
|
||||
</div>
|
||||
</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"
|
||||
style="line-height: 1.375rem;"
|
||||
>
|
||||
{{ item.reviewer }}
|
||||
</RouterLink>
|
||||
<div class="text-body-2">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.review="{ item }">
|
||||
<div class="my-4">
|
||||
<VRating
|
||||
:id="item.id"
|
||||
:name="`${item.id}`"
|
||||
readonly
|
||||
:model-value="item.review"
|
||||
size="24"
|
||||
class="mb-1"
|
||||
/>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ item.head }}
|
||||
</h6>
|
||||
<p class="text-sm text-wrap mb-0">
|
||||
{{ item.para }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.date="{ item }">
|
||||
{{ new Date(item.date).toDateString() }}
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="item.status === 'Published' ? 'success' : 'warning'"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="view"
|
||||
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.id } }"
|
||||
>
|
||||
View
|
||||
</VListItem>
|
||||
<VListItem
|
||||
value="delete"
|
||||
@click="deleteReview(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalReviews"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart";
|
||||
</style>
|
||||
535
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal file
535
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal file
@@ -0,0 +1,535 @@
|
||||
<script setup>
|
||||
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 orderData = ref()
|
||||
const route = useRoute('apps-ecommerce-order-details-id')
|
||||
const { data } = await useApi(`/apps/ecommerce/orders/${ route.params.id }`)
|
||||
if (data.value)
|
||||
orderData.value = data.value
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
||||
const resolvePaymentStatus = payment => {
|
||||
if (payment === 1)
|
||||
return {
|
||||
text: 'Paid',
|
||||
color: 'success',
|
||||
}
|
||||
if (payment === 2)
|
||||
return {
|
||||
text: 'Pending',
|
||||
color: 'warning',
|
||||
}
|
||||
if (payment === 3)
|
||||
return {
|
||||
text: 'Cancelled',
|
||||
color: 'secondary',
|
||||
}
|
||||
if (payment === 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 userData = {
|
||||
id: null,
|
||||
fullName: orderData.value?.customer,
|
||||
company: 'Pixinvent',
|
||||
role: 'Web developer',
|
||||
username: 'T1940',
|
||||
country: 'United States',
|
||||
contact: '+1 (609) 972-22-22',
|
||||
email: orderData.value?.email,
|
||||
status: 'Active',
|
||||
taxId: 'Tax-8894',
|
||||
language: 'English',
|
||||
currentPlan: '',
|
||||
avatar: '',
|
||||
taskDone: null,
|
||||
projectDone: null,
|
||||
}
|
||||
|
||||
const currentBillingAddress = {
|
||||
fullName: orderData.value?.customer,
|
||||
firstName: orderData.value?.customer.split(' ')[0],
|
||||
lastName: orderData.value?.customer.split(' ')[1],
|
||||
selectedCountry: 'USA',
|
||||
addressLine1: '45 Rocker Terrace',
|
||||
addressLine2: 'Latheronwheel',
|
||||
landmark: 'KW5 8NW, London',
|
||||
contact: '+1 (609) 972-22-22',
|
||||
country: 'USA',
|
||||
city: 'London',
|
||||
state: 'London',
|
||||
zipCode: 110001,
|
||||
}
|
||||
|
||||
const orderDetail = [
|
||||
{
|
||||
productName: 'OnePlus 7 Pro',
|
||||
productImage: product21,
|
||||
subtitle: 'Storage: 128gb',
|
||||
price: 799,
|
||||
quantity: 1,
|
||||
total: 799,
|
||||
},
|
||||
{
|
||||
productName: 'Face Cream',
|
||||
productImage: product22,
|
||||
subtitle: 'Gender: Women',
|
||||
price: 89,
|
||||
quantity: 1,
|
||||
total: 89,
|
||||
},
|
||||
{
|
||||
productName: 'Wooden Chair',
|
||||
productImage: product23,
|
||||
subtitle: 'Material: Woodem',
|
||||
price: 289,
|
||||
quantity: 2,
|
||||
total: 578,
|
||||
},
|
||||
{
|
||||
productName: 'Nike Jorden',
|
||||
productImage: product24,
|
||||
subtitle: 'Size: 8UK',
|
||||
price: 299,
|
||||
quantity: 2,
|
||||
total: 598,
|
||||
},
|
||||
]
|
||||
</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
|
||||
v-if="orderData?.payment"
|
||||
variant="tonal"
|
||||
:color="resolvePaymentStatus(orderData.payment)?.color"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
{{ resolvePaymentStatus(orderData.payment)?.text }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="orderData?.status"
|
||||
v-bind="resolveStatus(orderData?.status)"
|
||||
label
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
Aug 17, 2020, 5:48 (ET)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
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>
|
||||
<div class="text-base font-weight-medium text-primary cursor-pointer">
|
||||
Edit
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDivider />
|
||||
<VDataTable
|
||||
:headers="headers"
|
||||
:items="orderDetail"
|
||||
item-value="productName"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
>
|
||||
<template #item.productName="{ item }">
|
||||
<div class="d-flex gap-x-3 align-center">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:image="item.productImage"
|
||||
:rounded="0"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-column align-start">
|
||||
<h6 class="text-h6">
|
||||
{{ item.productName }}
|
||||
</h6>
|
||||
|
||||
<span class="text-body-2">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.price="{ item }">
|
||||
<div class="text-body-1">
|
||||
${{ item.price }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.total="{ item }">
|
||||
<div class="text-body-1">
|
||||
${{ item.total }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.quantity="{ item }">
|
||||
<div class="text-body-1">
|
||||
{{ item.quantity }}
|
||||
</div>
|
||||
</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 Total: </td>
|
||||
<td class="font-weight-medium">
|
||||
$2
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tax: </td>
|
||||
<td class="font-weight-medium">
|
||||
$28
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-high-emphasis 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"
|
||||
line-inset="9"
|
||||
align="start"
|
||||
side="end"
|
||||
line-color="primary"
|
||||
density="compact"
|
||||
>
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div class="app-timeline-title">
|
||||
Order was placed (Order ID: #32543)
|
||||
</div>
|
||||
<div class="app-timeline-meta">
|
||||
Tuesday 10:20 AM
|
||||
</div>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0 mt-3">
|
||||
Your order has been placed successfully
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<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 mt-3">
|
||||
Pick-up scheduled with courier
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<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 mt-3">
|
||||
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">
|
||||
<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 mt-3">
|
||||
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">
|
||||
<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 mt-3">
|
||||
Package has left an Amazon facility , NY
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="secondary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="app-timeline-title">Delivery</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-4 mt-3">
|
||||
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
|
||||
v-if="orderData"
|
||||
:variant="!orderData?.avatar.length ? 'tonal' : undefined"
|
||||
:rounded="1"
|
||||
class="me-3"
|
||||
>
|
||||
<VImg
|
||||
v-if="orderData?.avatar"
|
||||
:src="orderData?.avatar"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="font-weight-medium"
|
||||
>{{ avatarText(orderData?.customer) }}</span>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
{{ orderData?.customer }}
|
||||
</h6>
|
||||
<div class="text-body-1">
|
||||
Customer ID: #{{ orderData?.order }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-x-3 align-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="success"
|
||||
>
|
||||
<VIcon icon="tabler-shopping-cart" />
|
||||
</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 align-center">
|
||||
<h6 class="text-h6">
|
||||
Contact Info
|
||||
</h6>
|
||||
<div
|
||||
class="text-base text-primary cursor-pointer font-weight-medium"
|
||||
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
<span>Email: {{ orderData?.email }}</span>
|
||||
<span>Mobile: +1 (609) 972-22-22</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Shipping Address -->
|
||||
<VCard class="mb-6">
|
||||
<VCardItem>
|
||||
<VCardTitle>Shipping Address</VCardTitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div
|
||||
class="text-base font-weight-medium text-primary cursor-pointer"
|
||||
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<div class="text-body-1">
|
||||
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 mb-2">
|
||||
<h5 class="text-h5">
|
||||
Billing Address
|
||||
</h5>
|
||||
<div
|
||||
class="text-base font-weight-medium text-primary cursor-pointer"
|
||||
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
45 Rocker Terrace <br> Latheronwheel <br> KW5 8NW, London <br> UK
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h5 class="text-h5 mb-1">
|
||||
Mastercard
|
||||
</h5>
|
||||
<div class="text-body-1">
|
||||
Card Number: ******{{ orderData?.methodNumber }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:is-dialog-visible="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:is-dialog-visible="isUserInfoEditDialogVisible"
|
||||
:user-data="userData"
|
||||
/>
|
||||
|
||||
<AddEditAddressDialog
|
||||
v-model:is-dialog-visible="isEditAddressDialogVisible"
|
||||
:billing-address="currentBillingAddress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
386
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal file
386
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
import masterCardDark from '@images/icons/payments/img/master-dark.png'
|
||||
import masterCardLight from '@images/icons/payments/img/mastercard.png'
|
||||
import paypalDark from '@images/icons/payments/img/paypal-dark.png'
|
||||
import paypalLight from '@images/icons/payments/img/paypal-light.png'
|
||||
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Pending Payment',
|
||||
value: 56,
|
||||
icon: 'tabler-calendar-stats',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: 12689,
|
||||
icon: 'tabler-checks',
|
||||
},
|
||||
{
|
||||
title: 'Refunded',
|
||||
value: 124,
|
||||
icon: 'tabler-wallet',
|
||||
},
|
||||
{
|
||||
title: 'Failed',
|
||||
value: 32,
|
||||
icon: 'tabler-alert-octagon',
|
||||
},
|
||||
])
|
||||
|
||||
const mastercard = useGenerateImageVariant(masterCardLight, masterCardDark)
|
||||
const paypal = useGenerateImageVariant(paypalLight, paypalDark)
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
const selectedRows = 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: 'Action',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const updateOptions = options => {
|
||||
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' })
|
||||
|
||||
// Delete from selectedRows
|
||||
const index = selectedRows.value.findIndex(row => row === id)
|
||||
if (index !== -1)
|
||||
selectedRows.value.splice(index, 1)
|
||||
|
||||
// Refetch Orders
|
||||
fetchOrders()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard class="mb-6">
|
||||
<!-- 👉 Widgets -->
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs
|
||||
? id !== widgetData.length - 1 ? 'border-b pb-4' : ''
|
||||
: $vuetify.display.sm
|
||||
? id < (widgetData.length / 2) ? 'border-b pb-4' : ''
|
||||
: ''"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
|
||||
<div class="text-body-1">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="42"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="26"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
|
||||
: $vuetify.display.smAndUp ? id % 2 === 0
|
||||
: false"
|
||||
vertical
|
||||
inset
|
||||
length="60"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard>
|
||||
<!-- 👉 Filters -->
|
||||
<VCardText>
|
||||
<div class="d-flex justify-sm-space-between justify-start flex-wrap gap-4">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Order"
|
||||
style=" max-inline-size: 200px; min-inline-size: 200px;"
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-x-4 align-center">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
style="min-inline-size: 6.25rem;"
|
||||
:items="[5, 10, 20, 50, 100]"
|
||||
/>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="tabler-upload"
|
||||
text="Export"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Order Table -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:model-value="selectedRows"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="orders"
|
||||
: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 gap-x-3">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:color="!item.avatar.length ? 'primary' : ''"
|
||||
:variant="!item.avatar.length ? 'tonal' : undefined"
|
||||
>
|
||||
<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">
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
<RouterLink
|
||||
:to="{ name: 'pages-user-profile-tab', params: { tab: 'profile' } }"
|
||||
class="text-link"
|
||||
>
|
||||
{{ item.customer }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Payments -->
|
||||
<template #item.payment="{ item }">
|
||||
<div
|
||||
:class="`text-${resolvePaymentStatus(item.payment)?.color}`"
|
||||
class="font-weight-medium d-flex align-center gap-x-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-circle-filled"
|
||||
size="10"
|
||||
/>
|
||||
<div style="line-height: 22px;">
|
||||
{{ resolvePaymentStatus(item.payment)?.text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
label
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Method -->
|
||||
<template #item.method="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<img
|
||||
:src="item.method === 'mastercard' ? mastercard : paypal"
|
||||
height="18"
|
||||
>
|
||||
<div class="text-body-1">
|
||||
...{{ item.method === 'mastercard' ? item.methodNumber : '@gmail.com' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="view"
|
||||
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }"
|
||||
>
|
||||
View
|
||||
</VListItem>
|
||||
<VListItem
|
||||
value="delete"
|
||||
@click="deleteOrder(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalOrder"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-title:hover {
|
||||
color: rgba(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.product-widget {
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
570
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal file
570
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal file
@@ -0,0 +1,570 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const optionCounter = ref(1)
|
||||
const activeTab = ref('Restock')
|
||||
const isTaxChargeToProduct = ref(true)
|
||||
|
||||
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: 'tabler-cube',
|
||||
title: 'Restock',
|
||||
value: 'Restock',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-car',
|
||||
title: 'Shipping',
|
||||
value: 'Shipping',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-map-pin',
|
||||
title: 'Global Delivery',
|
||||
value: 'Global Delivery',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-world',
|
||||
title: 'Attributes',
|
||||
value: 'Attributes',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-lock',
|
||||
title: 'Advanced',
|
||||
value: 'Advanced',
|
||||
},
|
||||
]
|
||||
|
||||
const content = ref(`<p>
|
||||
Keep your account secure with authentication step.
|
||||
</p>`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex flex-wrap justify-start justify-sm-space-between gap-y-4 gap-x-6 mb-6">
|
||||
<div class="d-flex flex-column justify-center">
|
||||
<h4 class="text-h4 font-weight-medium">
|
||||
Add a new product
|
||||
</h4>
|
||||
<div class="text-body-1">
|
||||
Orders placed across your store
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
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">
|
||||
<AppTextField
|
||||
label="Name"
|
||||
placeholder="iPhone 14"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="SKU"
|
||||
placeholder="FXSK123U"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="Barcode"
|
||||
placeholder="0123-4567"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<span class="mb-1">Description (optional)</span>
|
||||
<ProductDescriptionEditor
|
||||
v-model="content"
|
||||
placeholder="Product Description"
|
||||
class="border rounded"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Media -->
|
||||
<VCard class="mb-6">
|
||||
<VCardItem>
|
||||
<template #title>
|
||||
Product Image
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="text-primary font-weight-medium text-sm cursor-pointer">Add Media from URL</span>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<DropZone />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Variants -->
|
||||
<VCard
|
||||
title="Variants"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<template
|
||||
v-for="i in optionCounter"
|
||||
:key="i"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AppSelect
|
||||
:items="['Size', 'Color', 'Weight']"
|
||||
placeholder="Select Variant"
|
||||
label="Options"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
class="d-flex align-self-end"
|
||||
>
|
||||
<AppTextField
|
||||
placeholder="38"
|
||||
type="number"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<VBtn
|
||||
class="mt-6"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="optionCounter++"
|
||||
>
|
||||
Add another option
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Inventory -->
|
||||
<VCard
|
||||
title="Inventory"
|
||||
class="inventory-card"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<div class="pe-3">
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
direction="vertical"
|
||||
color="primary"
|
||||
class="v-tabs-pill"
|
||||
>
|
||||
<VTab
|
||||
v-for="(tab, index) in inventoryTabsData"
|
||||
:key="index"
|
||||
>
|
||||
<VIcon
|
||||
:icon="tab.icon"
|
||||
class="me-2"
|
||||
/>
|
||||
<div class="text-truncate font-weight-medium text-start">
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VDivider :vertical="!$vuetify.display.smAndDown" />
|
||||
|
||||
<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 ps-3">
|
||||
<p class="mb-0">
|
||||
Options
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-4 align-center">
|
||||
<AppTextField
|
||||
label="Add to Stock"
|
||||
placeholder="Quantity"
|
||||
/>
|
||||
<VBtn class="align-self-end">
|
||||
Confirm
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-base text-high-emphasis pb-2">
|
||||
Product in stock now: 54
|
||||
</div>
|
||||
<div class="text-base text-high-emphasis pb-2">
|
||||
Product in transit: 390
|
||||
</div>
|
||||
<div class="text-base text-high-emphasis pb-2">
|
||||
Last time restocked: 24th June, 2022
|
||||
</div>
|
||||
<div class="text-base text-high-emphasis pb-2">
|
||||
Total stock over lifetime: 2,430
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Shipping">
|
||||
<VRadioGroup
|
||||
v-model="shippingType"
|
||||
label="Shipping Type"
|
||||
class="ms-3"
|
||||
>
|
||||
<VRadio
|
||||
v-for="item in shippingList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="mb-4"
|
||||
>
|
||||
<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 class="ps-3">
|
||||
<h5 class="text-h5 mb-6">
|
||||
Global Delivery
|
||||
</h5>
|
||||
|
||||
<VRadioGroup
|
||||
v-model="deliveryType"
|
||||
label="Global Delivery"
|
||||
>
|
||||
<VRadio
|
||||
value="Worldwide delivery"
|
||||
class="mb-4"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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="ps-3">
|
||||
<div class="mb-6 text-h6">
|
||||
Attributes
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
label="Fragile Product"
|
||||
value="Fragile Product"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Biodegradable"
|
||||
label="Biodegradable"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Frozen Product"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div>Frozen Product</div>
|
||||
<VTextField
|
||||
placeholder="40 C"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VCheckbox>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Expiry Date"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Advanced">
|
||||
<div class="ps-3">
|
||||
<h5 class="text-h5 mb-6">
|
||||
Advanced
|
||||
</h5>
|
||||
<div class="d-flex flex-sm-row flex-column flex-wrap justify-space-between gap-x-6 gap-y-4">
|
||||
<AppSelect
|
||||
label="Product ID Type"
|
||||
placeholder="Select Product Type"
|
||||
:items="['ISBN', 'UPC', 'EAN', 'JAN']"
|
||||
/>
|
||||
<AppTextField
|
||||
label="Product Id"
|
||||
placeholder="100023"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
md="4"
|
||||
cols="12"
|
||||
>
|
||||
<!-- 👉 Pricing -->
|
||||
<VCard
|
||||
title="Pricing"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
label="Best Price"
|
||||
placeholder="Price"
|
||||
class="mb-6"
|
||||
/>
|
||||
<AppTextField
|
||||
label="Discounted Price"
|
||||
placeholder="$499"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<VCheckbox
|
||||
v-model="isTaxChargeToProduct"
|
||||
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 density="compact" />
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Organize -->
|
||||
<VCard title="Organize">
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column gap-y-4">
|
||||
<AppSelect
|
||||
placeholder="Select Vendor"
|
||||
label="Vendor"
|
||||
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
|
||||
/>
|
||||
<div>
|
||||
<VLabel class="d-flex">
|
||||
<div class="d-flex text-sm justify-space-between w-100">
|
||||
<div class="text-high-emphasis">
|
||||
Category
|
||||
</div>
|
||||
</div>
|
||||
</VLabel>
|
||||
|
||||
<div class="d-flex gap-x-4">
|
||||
<AppSelect
|
||||
placeholder="Select Category"
|
||||
:items="['Household', 'Office', 'Electronics', 'Management', 'Automotive']"
|
||||
/>
|
||||
<VBtn
|
||||
rounded
|
||||
icon="tabler-plus"
|
||||
variant="tonal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AppSelect
|
||||
placeholder="Select Collection"
|
||||
label="Collection"
|
||||
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
|
||||
/>
|
||||
<AppSelect
|
||||
placeholder="Select Status"
|
||||
label="Status"
|
||||
:items="['Published', 'Inactive', 'Scheduled']"
|
||||
/>
|
||||
<AppTextField
|
||||
label="Tags"
|
||||
placeholder="Fashion, Trending, Summer"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drop-zone {
|
||||
border: 2px dashed rgba(var(--v-theme-on-surface), 0.12);
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.inventory-card {
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-slide-group-item--active.v-tab--selected.text-primary {
|
||||
h6 {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
255
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal file
255
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<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 = ref([
|
||||
{
|
||||
id: 1,
|
||||
categoryTitle: 'Smart Phone',
|
||||
description: 'Choose from wide range of smartphones online at best prices.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
categoryTitle: 'Clothing, Shoes, and jewellery',
|
||||
description: 'Fashion for a wide selection of clothing, shoes, jewellery and watches.',
|
||||
totalProduct: 4689,
|
||||
totalEarning: 45627,
|
||||
image: product9,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
categoryTitle: 'Home and Kitchen',
|
||||
description: 'Browse through the wide range of Home and kitchen products.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product10,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
categoryTitle: 'Beauty and Personal Care',
|
||||
description: 'Explore beauty and personal care products, shop makeup and etc.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product19,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
categoryTitle: 'Books',
|
||||
description: 'Over 25 million titles across categories such as business and etc.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product25,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
categoryTitle: 'Games',
|
||||
description: 'Every month, get exclusive in-game loot, free games, a free subscription.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product12,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
categoryTitle: 'Baby Products',
|
||||
description: 'Buy baby products across different categories from top brands.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product14,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
categoryTitle: 'Grocery',
|
||||
description: 'Shop grocery Items through at best prices in India.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product28,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
categoryTitle: 'Computer Accessories',
|
||||
description: 'Enhance your computing experience with our range of computer accessories.',
|
||||
totalProduct: 9876,
|
||||
totalEarning: 65421,
|
||||
image: product17,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
categoryTitle: 'Fitness Tracker',
|
||||
description: 'Monitor your health and fitness goals with our range of advanced fitness trackers.',
|
||||
totalProduct: 1987,
|
||||
totalEarning: 32067,
|
||||
image: product10,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
categoryTitle: 'Smart Home Devices',
|
||||
description: 'Transform your home into a smart home with our innovative smart home devices.',
|
||||
totalProduct: 2345,
|
||||
totalEarning: 87654,
|
||||
image: product11,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
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',
|
||||
},
|
||||
{
|
||||
title: 'Total Earning',
|
||||
key: 'totalEarning',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const searchQuery = ref('')
|
||||
const isAddProductDrawerOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-sm-space-between flex-wrap gap-y-4 gap-x-6 justify-start">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Category"
|
||||
style="max-inline-size: 280px; min-inline-size: 280px;"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[5, 10, 15]"
|
||||
style="max-inline-size: 100px; min-inline-size: 100px;"
|
||||
/>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isAddProductDrawerOpen = !isAddProductDrawerOpen"
|
||||
>
|
||||
Add Category
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="category-table">
|
||||
<VDataTable
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="categoryData"
|
||||
item-value="categoryTitle"
|
||||
:search="searchQuery"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
>
|
||||
<template #item.actions>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-edit"
|
||||
size="22"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-dots-vertical"
|
||||
size="22"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<template #item.categoryTitle="{ item }">
|
||||
<div class="d-flex gap-x-3 align-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="38"
|
||||
>
|
||||
<img
|
||||
:src="item.image"
|
||||
:alt="item.categoryTitle"
|
||||
width="38"
|
||||
height="38"
|
||||
>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
{{ item.categoryTitle }}
|
||||
</h6>
|
||||
<div class="text-body-2">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.totalEarning="{ item }">
|
||||
<div class="text-body-1 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>
|
||||
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="categoryData.length"
|
||||
/>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<ECommerceAddCategoryDrawer v-model:is-drawer-open="isAddProductDrawerOpen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.category-table {
|
||||
.v-table {
|
||||
th:nth-child(3),
|
||||
th:nth-child(4) {
|
||||
.v-data-table-header__content {
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
495
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal file
495
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal file
@@ -0,0 +1,495 @@
|
||||
<script setup>
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'In-Store Sales',
|
||||
value: '$5,345',
|
||||
icon: 'tabler-smart-home',
|
||||
desc: '5k orders',
|
||||
change: 5.7,
|
||||
},
|
||||
{
|
||||
title: 'Website Sales',
|
||||
value: '$674,347',
|
||||
icon: 'tabler-device-laptop',
|
||||
desc: '21k orders',
|
||||
change: 12.4,
|
||||
},
|
||||
{
|
||||
title: 'Discount',
|
||||
value: '$14,235',
|
||||
icon: 'tabler-gift',
|
||||
desc: '6k orders',
|
||||
},
|
||||
{
|
||||
title: 'Affiliate',
|
||||
value: '$8,345',
|
||||
icon: 'tabler-wallet',
|
||||
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 selectedRows = 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 => {
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const resolveCategory = category => {
|
||||
if (category === 'Accessories')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'tabler-device-watch',
|
||||
}
|
||||
if (category === 'Home Decor')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'tabler-home',
|
||||
}
|
||||
if (category === 'Electronics')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'tabler-device-imac',
|
||||
}
|
||||
if (category === 'Shoes')
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'tabler-shoe',
|
||||
}
|
||||
if (category === 'Office')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'tabler-briefcase',
|
||||
}
|
||||
if (category === 'Games')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'tabler-device-gamepad-2',
|
||||
}
|
||||
}
|
||||
|
||||
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' })
|
||||
|
||||
// Delete from selectedRows
|
||||
const index = selectedRows.value.findIndex(row => row === id)
|
||||
if (index !== -1)
|
||||
selectedRows.value.splice(index, 1)
|
||||
|
||||
// Refetch products
|
||||
fetchProducts()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 👉 widgets -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-3">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs
|
||||
? id !== widgetData.length - 1 ? 'border-b pb-4' : ''
|
||||
: $vuetify.display.sm
|
||||
? id < (widgetData.length / 2) ? 'border-b pb-4' : ''
|
||||
: ''"
|
||||
>
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<div class="text-body-1 text-capitalize">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<div class="text-no-wrap">
|
||||
{{ data.desc }}
|
||||
</div>
|
||||
|
||||
<VChip
|
||||
v-if="data.change"
|
||||
label
|
||||
:color="data.change > 0 ? 'success' : 'error'"
|
||||
size="small"
|
||||
>
|
||||
{{ prefixWithPlus(data.change) }}%
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="44"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="28"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
|
||||
: $vuetify.display.smAndUp ? id % 2 === 0
|
||||
: false"
|
||||
vertical
|
||||
inset
|
||||
length="92"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 products -->
|
||||
<VCard
|
||||
title="Filters"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 👉 Select Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Status"
|
||||
:items="status"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Category -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedCategory"
|
||||
placeholder="Category"
|
||||
:items="categories"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Stock Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedStock"
|
||||
placeholder="Stock"
|
||||
:items="stockStatus"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex flex-wrap gap-4 ma-6">
|
||||
<div class="d-flex align-center">
|
||||
<!-- 👉 Search -->
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Product"
|
||||
style="inline-size: 200px;"
|
||||
class="me-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
<div class="d-flex gap-4 flex-wrap align-center">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[5, 10, 20, 25, 50]"
|
||||
/>
|
||||
<!-- 👉 Export button -->
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="tabler-upload"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="$router.push('/apps/ecommerce/product/add')"
|
||||
>
|
||||
Add Product
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="mt-4" />
|
||||
|
||||
<!-- 👉 Datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:model-value="selectedRows"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
show-select
|
||||
:items="products"
|
||||
:items-length="totalProduct"
|
||||
class="text-no-wrap"
|
||||
@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-body-1 font-weight-medium text-high-emphasis">{{ item.productName }}</span>
|
||||
<span class="text-body-2">{{ 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-body-1 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)"
|
||||
density="default"
|
||||
label
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-edit" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="download"
|
||||
prepend-icon="tabler-download"
|
||||
>
|
||||
Download
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="delete"
|
||||
prepend-icon="tabler-trash"
|
||||
@click="deleteProduct(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="duplicate"
|
||||
prepend-icon="tabler-copy"
|
||||
>
|
||||
Duplicate
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalProduct"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
393
resources/js/pages/apps/ecommerce/referrals.vue
Normal file
393
resources/js/pages/apps/ecommerce/referrals.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<script setup>
|
||||
import paperImg from '@images/svg/paper-send.svg?raw'
|
||||
import rocketImg from '@images/svg/rocket.svg?raw'
|
||||
import userInfoImg from '@images/svg/user-info.svg?raw'
|
||||
|
||||
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: 'tabler-currency-dollar',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Unpaid Earning',
|
||||
value: '$8,647',
|
||||
icon: 'tabler-gift',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Signup',
|
||||
value: '2,367',
|
||||
icon: 'tabler-users',
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Conversion Rate',
|
||||
value: '4.5%',
|
||||
icon: 'tabler-infinity',
|
||||
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 => {
|
||||
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 resolveAvatarbg = status => {
|
||||
if (status === 'Rejected')
|
||||
return { color: 'error' }
|
||||
if (status === 'Unpaid')
|
||||
return { color: 'warning' }
|
||||
if (status === 'Paid')
|
||||
return { color: 'success' }
|
||||
}
|
||||
|
||||
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 justify-space-between align-center">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="text-h5 mb-1">
|
||||
{{ data.value }}
|
||||
</h5>
|
||||
<div class="text-body-2">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="40"
|
||||
variant="tonal"
|
||||
:color="data.color"
|
||||
>
|
||||
<VIcon :icon="data.icon" />
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Icon Steps -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle class="mb-1">
|
||||
How to use
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
Integrate your referral code in 3 easy steps.
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column flex-sm-row gap-6 justify-sm-space-between align-center">
|
||||
<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">
|
||||
<VIcon
|
||||
:icon="step.icon"
|
||||
color="primary"
|
||||
size="36"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-body-1 text-wrap text-center">
|
||||
{{ step.desc }}
|
||||
</div>
|
||||
<h6 class="text-primary text-h6">
|
||||
{{ step.value }}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Invite -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="mb-5">
|
||||
<h5 class="text-h5 mb-5">
|
||||
Invite your friends
|
||||
</h5>
|
||||
<div class="d-flex align-center flex-wrap gap-4 flex-wrap">
|
||||
<AppTextField
|
||||
label="Enter friend’s email address and invite them"
|
||||
placeholder="Email Address"
|
||||
/>
|
||||
<VBtn class="align-self-end">
|
||||
Submit
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-h5 mb-5">
|
||||
Share the referral link
|
||||
</h5>
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
<AppTextField
|
||||
label="Share referral link in social media"
|
||||
placeholder="pixinvent.com/?ref=6478"
|
||||
/>
|
||||
<div class="d-flex align-self-end gap-x-2">
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#3B5998"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="tabler-brand-facebook"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#55ACEE"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="tabler-brand-twitter"
|
||||
/>
|
||||
</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>
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
<AppSelect
|
||||
v-model="itemsPerPage"
|
||||
:items="[10, 25, 50, 100]"
|
||||
style="inline-size: 100px;"
|
||||
/>
|
||||
<VBtn
|
||||
prepend-icon="tabler-upload"
|
||||
color="default"
|
||||
variant="tonal"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:items="referrals"
|
||||
:headers="headers"
|
||||
:items-length="totalReferrals"
|
||||
show-select
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.users="{ item }">
|
||||
<div class="d-flex align-center gap-x-4">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:variant="!item.avatar ? 'tonal' : undefined"
|
||||
:color="!item.avatar ? resolveAvatarbg(item.status)?.color : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.user) }}</span>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="font-weight-medium text-high-emphasis">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: 478426 } }"
|
||||
class="text-link"
|
||||
>
|
||||
{{ item.user }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.referred-id="{ item }">
|
||||
{{ item.referredId }}
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
label
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item.earning="{ item }">
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
{{ item.earning }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalReferrals"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px dashed rgb(var(--v-theme-primary));
|
||||
border-radius: 50%;
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #000;
|
||||
font-size: 42px;
|
||||
}
|
||||
</style>
|
||||
107
resources/js/pages/apps/ecommerce/settings.vue
Normal file
107
resources/js/pages/apps/ecommerce/settings.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<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: 'tabler-building-store',
|
||||
title: 'Store Details',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-credit-card',
|
||||
title: 'Payments',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-shopping-cart',
|
||||
title: 'Checkout',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-discount',
|
||||
title: 'Shipping & Delivery',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-map-pin',
|
||||
title: 'Location',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-bell-ringing',
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
.my-class {
|
||||
padding: 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
||||
}
|
||||
</style>
|
||||
557
resources/js/pages/apps/email/index.vue
Normal file
557
resources/js/pages/apps/email/index.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<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 emailsMeta = computed(() => emailData.value.emailsMeta)
|
||||
|
||||
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 = []
|
||||
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 })
|
||||
if (openedEmail.value)
|
||||
refreshOpenedEmail()
|
||||
else
|
||||
await fetchEmails()
|
||||
}
|
||||
|
||||
const handleMoveMailsTo = async action => {
|
||||
await moveSelectedEmailTo(action, selectedEmails.value)
|
||||
await fetchEmails()
|
||||
}
|
||||
|
||||
const handleEmailLabels = async labelTitle => {
|
||||
await updateEmailLabels(selectedEmails.value, labelTitle)
|
||||
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=" z-index: 0;min-block-size: 100%;"
|
||||
class="email-app-layout"
|
||||
>
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
data-allow-mismatch
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
:temporary="$vuetify.display.mdAndDown"
|
||||
>
|
||||
<EmailLeftSidebarContent
|
||||
:emails-meta="emailsMeta"
|
||||
@toggle-compose-dialog-visibility="isComposeDialogVisible = !isComposeDialogVisible"
|
||||
/>
|
||||
</VNavigationDrawer>
|
||||
<EmailView
|
||||
:email="openedEmail"
|
||||
:email-meta="emailViewMeta"
|
||||
@refresh="refreshOpenedEmail"
|
||||
@navigated="changeOpenedEmail"
|
||||
@close="openedEmail = null"
|
||||
@trash="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="tabler-menu-2" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Search -->
|
||||
<VTextField
|
||||
v-model="q"
|
||||
density="default"
|
||||
class="email-search px-sm-2 flex-grow-1 py-1"
|
||||
placeholder="Search mail"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<VIcon
|
||||
icon="tabler-search"
|
||||
size="24"
|
||||
class="me-1 text-medium-emphasis"
|
||||
/>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
<VDivider />
|
||||
<!-- 👉 Action bar -->
|
||||
<div class="py-2 px-4 d-flex align-center d-flex gap-x-1">
|
||||
<!-- TODO: Make checkbox primary on indeterminate state -->
|
||||
<VCheckbox
|
||||
:model-value="selectAllEmailCheckbox"
|
||||
:indeterminate="isSelectAllEmailCheckboxIndeterminate"
|
||||
class="d-flex"
|
||||
@update:model-value="selectAllCheckboxUpdate"
|
||||
/>
|
||||
<div
|
||||
class="w-100 d-flex align-center action-bar-actions gap-x-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="tabler-trash"
|
||||
size="22"
|
||||
/>
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Delete Mail
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
<!-- Mark unread/read -->
|
||||
<IconBtn @click="isAllMarkRead ? handleActionClick('unread') : handleActionClick('read') ">
|
||||
<VIcon
|
||||
:icon="isAllMarkRead ? 'tabler-mail' : 'tabler-mail-opened'"
|
||||
size="22"
|
||||
/>
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ isAllMarkRead ? 'Mark as Unread' : 'Mark as Read' }}
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
<!-- Move to folder -->
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-folder"
|
||||
size="22"
|
||||
/>
|
||||
<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'"
|
||||
href="#"
|
||||
class="items-center"
|
||||
@click="handleMoveMailsTo(moveTo.action)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="moveTo.icon"
|
||||
class="me-2"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="text-capitalize">
|
||||
{{ moveTo.action }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<!-- Update labels -->
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-tag"
|
||||
size="22"
|
||||
/>
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Label
|
||||
</VTooltip>
|
||||
<VMenu activator="parent">
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="label in labels"
|
||||
:key="label.title"
|
||||
href="#"
|
||||
@click="handleEmailLabels(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 @click="fetchEmails">
|
||||
<VIcon
|
||||
icon="tabler-refresh"
|
||||
size="22"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-dots-vertical"
|
||||
size="22"
|
||||
/>
|
||||
</IconBtn>
|
||||
</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="tabler-star"
|
||||
size="22"
|
||||
/>
|
||||
</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="tabler-circle-filled"
|
||||
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="tabler-trash"
|
||||
size="22"
|
||||
/>
|
||||
<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 ? 'tabler-mail' : 'tabler-mail-opened'"
|
||||
size="22"
|
||||
/>
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ email.isRead ? 'Mark as Unread' : 'Mark as Read' }}
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
<IconBtn @click.stop="handleActionClick('spam', [email.id])">
|
||||
<VIcon
|
||||
icon="tabler-info-circle"
|
||||
size="22"
|
||||
/>
|
||||
<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";
|
||||
@use "@core-scss/base/mixins";
|
||||
|
||||
// ℹ️ Remove border. Using variant plain cause UI issue, caret isn't align in center
|
||||
.email-search {
|
||||
.v-field__outline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-field__field {
|
||||
.v-field__input {
|
||||
font-size: 0.9375rem !important;
|
||||
line-height: 1.375rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
block-size: 4.375rem;
|
||||
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;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.email-actions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
171
resources/js/pages/apps/invoice/add/index.vue
Normal file
171
resources/js/pages/apps/invoice/add/index.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<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="tabler-send"
|
||||
class="mb-4"
|
||||
@click="isSendPaymentSidebarVisible = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Preview -->
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: '5036' } }"
|
||||
>
|
||||
Preview
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Save -->
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Select payment method -->
|
||||
<AppSelect
|
||||
id="payment-method"
|
||||
v-model="selectedPaymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Accept Payment Via"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<!-- 👉 Payment Terms -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<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">
|
||||
<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:is-drawer-open="isSendPaymentSidebarVisible" />
|
||||
</template>
|
||||
177
resources/js/pages/apps/invoice/edit/[id].vue
Normal file
177
resources/js/pages/apps/invoice/edit/[id].vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<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 }`)
|
||||
if (invoiceDetails.value) {
|
||||
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 v-if="invoiceData && invoiceData?.invoice">
|
||||
<!-- 👉 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="tabler-send"
|
||||
class="mb-4"
|
||||
@click="isSendSidebarActive = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<!-- 👉 Preview button -->
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
class="flex-grow-1"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: route.params.id } }"
|
||||
>
|
||||
Preview
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Save button -->
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
class="mb-4 flex-grow-1"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Add Payment trigger button -->
|
||||
<VBtn
|
||||
block
|
||||
color="success"
|
||||
prepend-icon="tabler-currency-dollar"
|
||||
@click="isAddPaymentSidebarActive = true"
|
||||
>
|
||||
Add Payment
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Accept payment via -->
|
||||
<AppSelect
|
||||
id="payment-method"
|
||||
v-model="selectedPaymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Accept Payment Via"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- 👉 Payment Terms -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<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">
|
||||
<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:is-drawer-open="isSendSidebarActive" />
|
||||
|
||||
<!-- 👉 Invoice add payment drawer -->
|
||||
<InvoiceAddPaymentDrawer v-model:is-drawer-open="isAddPaymentSidebarActive" />
|
||||
</VRow>
|
||||
|
||||
<section v-else>
|
||||
<VAlert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
Invoice with ID {{ route.params.id }} not found!
|
||||
</VAlert>
|
||||
</section>
|
||||
</template>
|
||||
442
resources/js/pages/apps/invoice/list/index.vue
Normal file
442
resources/js/pages/apps/invoice/list/index.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<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 => {
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Clients',
|
||||
value: 24,
|
||||
icon: 'tabler-user',
|
||||
},
|
||||
{
|
||||
title: 'Invoices',
|
||||
value: 165,
|
||||
icon: 'tabler-file-invoice',
|
||||
},
|
||||
{
|
||||
title: 'Paid',
|
||||
value: '$2.46k',
|
||||
icon: 'tabler-checks',
|
||||
},
|
||||
{
|
||||
title: 'Unpaid',
|
||||
value: '$876',
|
||||
icon: 'tabler-circle-off',
|
||||
},
|
||||
])
|
||||
|
||||
// 👉 headers
|
||||
const headers = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
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: 'tabler-chart-pie-2',
|
||||
}
|
||||
if (status === 'Paid')
|
||||
return {
|
||||
variant: 'success',
|
||||
icon: 'tabler-check',
|
||||
}
|
||||
if (status === 'Downloaded')
|
||||
return {
|
||||
variant: 'info',
|
||||
icon: 'tabler-arrow-down',
|
||||
}
|
||||
if (status === 'Draft')
|
||||
return {
|
||||
variant: 'primary',
|
||||
icon: 'tabler-folder',
|
||||
}
|
||||
if (status === 'Sent')
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'tabler-mail',
|
||||
}
|
||||
if (status === 'Past Due')
|
||||
return {
|
||||
variant: 'error',
|
||||
icon: 'tabler-help',
|
||||
}
|
||||
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'tabler-x',
|
||||
}
|
||||
}
|
||||
|
||||
const computedMoreList = computed(() => {
|
||||
return paramId => [
|
||||
{
|
||||
title: 'Download',
|
||||
value: 'download',
|
||||
prependIcon: 'tabler-download',
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
value: 'edit',
|
||||
prependIcon: 'tabler-pencil',
|
||||
to: {
|
||||
name: 'apps-invoice-edit-id',
|
||||
params: { id: paramId },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
prependIcon: 'tabler-layers-intersect',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const deleteInvoice = async id => {
|
||||
await $api(`/apps/invoice/${ id }`, { method: 'DELETE' })
|
||||
|
||||
// Delete from selectedRows
|
||||
const index = selectedRows.value.findIndex(row => row === id)
|
||||
if (index !== -1)
|
||||
selectedRows.value.splice(index, 1)
|
||||
|
||||
// Refetch Invoices
|
||||
fetchInvoices()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="invoices">
|
||||
<!-- 👉 Invoice Widgets -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-3">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between align-center"
|
||||
:class="$vuetify.display.xs
|
||||
? id !== widgetData.length - 1 ? 'border-b pb-4' : ''
|
||||
: $vuetify.display.sm
|
||||
? id < (widgetData.length / 2) ? 'border-b pb-4' : ''
|
||||
: ''"
|
||||
>
|
||||
<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
|
||||
length="60"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard id="invoice-list">
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap gap-4">
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span>Show</span>
|
||||
<AppSelect
|
||||
:model-value="itemsPerPage"
|
||||
:items="[
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 25, title: '25' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: 100, title: '100' },
|
||||
{ value: -1, title: 'All' },
|
||||
]"
|
||||
style="inline-size: 5.5rem;"
|
||||
@update:model-value="itemsPerPage = parseInt($event, 10)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 👉 Create invoice -->
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
:to="{ name: 'apps-invoice-add' }"
|
||||
>
|
||||
Create invoice
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<!-- 👉 Search -->
|
||||
<div class="invoice-list-filter">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Invoice"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Select status -->
|
||||
<div class="invoice-list-filter">
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Invoice Status"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
single-line
|
||||
:items="['Downloaded', 'Draft', 'Sent', 'Paid', 'Partial Payment', 'Past Due']"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
|
||||
<!-- SECTION Datatable -->
|
||||
<VDataTableServer
|
||||
v-model:model-value="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"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- id -->
|
||||
<template #item.id="{ item }">
|
||||
<RouterLink :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
|
||||
#{{ item.id }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- status -->
|
||||
<template #item.status="{ 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-link font-weight-medium"
|
||||
>
|
||||
{{ item.client.name }}
|
||||
</RouterLink>
|
||||
<span class="text-sm text-medium-emphasis">{{ 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"
|
||||
label
|
||||
size="x-small"
|
||||
>
|
||||
{{ (resolveInvoiceBalanceVariant(item.balance, item.total)).status }}
|
||||
</VChip>
|
||||
|
||||
<template v-else>
|
||||
<span class="text-base text-high-emphasis">
|
||||
{{ Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status) > 0 ? `$${(resolveInvoiceBalanceVariant(item.balance, item.total)).status}` : `-$${Math.abs(Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status))}` }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn @click="deleteInvoice(item.id)">
|
||||
<VIcon icon="tabler-trash" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
|
||||
<VIcon icon="tabler-eye" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn
|
||||
:menu-list="computedMoreList(item.id)"
|
||||
item-props
|
||||
color="undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalInvoices"
|
||||
/>
|
||||
</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-filter {
|
||||
inline-size: 12rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
452
resources/js/pages/apps/invoice/preview/[id].vue
Normal file
452
resources/js/pages/apps/invoice/preview/[id].vue
Normal file
@@ -0,0 +1,452 @@
|
||||
<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 = ref()
|
||||
const paymentDetails = ref()
|
||||
if (invoiceData.value) {
|
||||
invoice.value = invoiceData.value.invoice
|
||||
paymentDetails.value = 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="invoice && paymentDetails">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="9"
|
||||
>
|
||||
<VCard class="invoice-preview-wrapper pa-6 pa-sm-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 app-logo mb-6">
|
||||
<!-- 👉 Logo -->
|
||||
<VNodeRenderer :nodes="themeConfig.app.logo" />
|
||||
|
||||
<!-- 👉 Title -->
|
||||
<h6 class="app-logo-title">
|
||||
{{ 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-4">
|
||||
Total Due:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.totalDue }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-4">
|
||||
Bank Name:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.bankName }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-4">
|
||||
Country:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.country }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-4">
|
||||
IBAN:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.iban }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-4">
|
||||
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 class="text-base">
|
||||
<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-base font-weight-medium">
|
||||
$1800
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Discount:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-base font-weight-medium">
|
||||
$28
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Tax:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-base font-weight-medium">
|
||||
21%
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<table class="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Total:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-base font-weight-medium">
|
||||
$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="tabler-send"
|
||||
class="mb-4"
|
||||
@click="isSendPaymentSidebarVisible = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Download
|
||||
</VBtn>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
class="flex-grow-1"
|
||||
@click="printInvoice"
|
||||
>
|
||||
Print
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
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="tabler-currency-dollar"
|
||||
color="success"
|
||||
@click="isAddPaymentSidebarVisible = true"
|
||||
>
|
||||
Add Payment
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Add Payment Sidebar -->
|
||||
<InvoiceAddPaymentDrawer v-model:is-drawer-open="isAddPaymentSidebarVisible" />
|
||||
|
||||
<!-- 👉 Send Invoice Sidebar -->
|
||||
<InvoiceSendInvoiceDrawer v-model:is-drawer-open="isSendPaymentSidebarVisible" />
|
||||
</section>
|
||||
<section v-else>
|
||||
<VAlert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
Invoice with ID {{ route.params.id }} not found!
|
||||
</VAlert>
|
||||
</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: 47, 43, 61;
|
||||
--v-theme-on-background: 47, 43, 61;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.vue-devtools__anchor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
85
resources/js/pages/apps/kanban/index.vue
Normal file
85
resources/js/pages/apps/kanban/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
import KanbanBoardComp from '@/views/apps/kanban/KanbanBoard.vue'
|
||||
|
||||
// 👉 initial kanban data fetch
|
||||
const {
|
||||
data: kanban,
|
||||
execute: refetchKanban,
|
||||
} = await useApi('/apps/kanban')
|
||||
|
||||
const addNewBoard = async newBoardName => {
|
||||
await $api('/apps/kanban/board/add', {
|
||||
method: 'POST',
|
||||
body: { title: newBoardName },
|
||||
})
|
||||
refetchKanban()
|
||||
}
|
||||
|
||||
const deleteBoard = async boardId => {
|
||||
await $api(`/apps/kanban/board/${ boardId }`, { method: 'DELETE' })
|
||||
refetchKanban()
|
||||
}
|
||||
|
||||
const renameTheBoard = async kanbanBoard => {
|
||||
await $api('/apps/kanban/board/rename', {
|
||||
method: 'PUT',
|
||||
body: kanbanBoard,
|
||||
})
|
||||
refetchKanban()
|
||||
}
|
||||
|
||||
const addNewItem = async newItem => {
|
||||
await $api('/apps/kanban/item/add', {
|
||||
method: 'POST',
|
||||
body: newItem,
|
||||
})
|
||||
refetchKanban()
|
||||
}
|
||||
|
||||
const editItemFn = async editItem => {
|
||||
await $api('/apps/kanban/item/update', {
|
||||
method: 'PUT',
|
||||
body: editItem,
|
||||
})
|
||||
refetchKanban()
|
||||
}
|
||||
|
||||
const deleteItemFn = async deleteItem => {
|
||||
if (deleteItem.item && deleteItem.item.id) {
|
||||
await $api(`/apps/kanban/item/${ deleteItem.item.id }`, {
|
||||
method: 'DELETE',
|
||||
body: deleteItem,
|
||||
})
|
||||
refetchKanban()
|
||||
}
|
||||
}
|
||||
|
||||
const updateItemState = async kanbanState => {
|
||||
await $api('/apps/kanban/item/state-update', {
|
||||
method: 'PUT',
|
||||
body: kanbanState,
|
||||
})
|
||||
}
|
||||
|
||||
const updateBoardState = async kanbanBoardIds => {
|
||||
await $api('/apps/kanban/board/state-update', {
|
||||
method: 'PUT',
|
||||
body: kanbanBoardIds,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KanbanBoardComp
|
||||
v-if="kanban"
|
||||
:kanban-data="kanban"
|
||||
@add-new-board="addNewBoard"
|
||||
@delete-board="deleteBoard"
|
||||
@rename-board="renameTheBoard"
|
||||
@add-new-item="addNewItem"
|
||||
@edit-item="editItemFn"
|
||||
@delete-item="deleteItemFn"
|
||||
@update-items-state="updateItemState"
|
||||
@update-board-state="updateBoardState"
|
||||
/>
|
||||
</template>
|
||||
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>
|
||||
402
resources/js/pages/apps/logistics/fleet.vue
Normal file
402
resources/js/pages/apps/logistics/fleet.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
// ❗ 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 = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
|
||||
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(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
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: 100,
|
||||
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"
|
||||
data-allow-mismatch
|
||||
width="360"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
border="none"
|
||||
>
|
||||
<VCard
|
||||
class="h-100 fleet-navigation-drawer"
|
||||
flat
|
||||
>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
Fleet
|
||||
</VCardTitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn
|
||||
class="d-lg-none navigation-close-btn"
|
||||
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
|
||||
>
|
||||
<VIcon icon="tabler-x" />
|
||||
</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 align-center">
|
||||
<VAvatar
|
||||
icon="tabler-car"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
{{ vehicle.name }}
|
||||
</div>
|
||||
<div class="text-body-1">
|
||||
{{ vehicle.location }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<IconBtn size="small">
|
||||
<VIcon
|
||||
:icon="showPanel[index] ? 'tabler-chevron-down' : $vuetify.locale.isRtl ? 'tabler-chevron-left' : 'tabler-chevron-right'"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VExpandTransition mode="out-in">
|
||||
<div v-show="showPanel[index]">
|
||||
<div class="py-8">
|
||||
<div class="d-flex justify-space-between mb-1">
|
||||
<span class="text-body-1 text-high-emphasis">Delivery Process</span>
|
||||
<span class="text-body-1">{{ vehicle.progress }}%</span>
|
||||
</div>
|
||||
<VProgressLinear
|
||||
:model-value="vehicle.progress"
|
||||
color="primary"
|
||||
rounded
|
||||
height="6"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<VTimeline
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
side="end"
|
||||
density="compact"
|
||||
line-thickness="1"
|
||||
line-inset="6"
|
||||
class="ps-2 v-timeline--variant-outlined fleet-timeline"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="tabler-circle-check"
|
||||
dot-color="rgb(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="ps-1">
|
||||
<div class="text-caption text-success">
|
||||
TRACKING NUMBER CREATED
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
{{ vehicle.driverName }}
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
Sep 01, 7:53 AM
|
||||
</div>
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="tabler-circle-check"
|
||||
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="text-body-2">
|
||||
Sep 03, 8:02 AM
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
icon="tabler-map-pin"
|
||||
dot-color="rgb(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
ARRIVED
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
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="tabler-menu-2" />
|
||||
</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";
|
||||
@use "@core-scss/base/mixins";
|
||||
@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;
|
||||
}
|
||||
|
||||
.fleet-navigation-drawer {
|
||||
.v-timeline .v-timeline-divider__dot .v-timeline-divider__inner-dot {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fleet-timeline {
|
||||
&.v-timeline .v-timeline-item:not(:last-child) {
|
||||
.v-timeline-item__body {
|
||||
margin-block-end: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-id-pattern */
|
||||
#mapContainer {
|
||||
block-size: 100vh !important;
|
||||
}
|
||||
</style>
|
||||
201
resources/js/pages/apps/permissions/index.vue
Normal file
201
resources/js/pages/apps/permissions/index.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<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 => {
|
||||
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>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center justify-space-between flex-wrap gap-4">
|
||||
<div class="d-flex gap-2 align-center">
|
||||
<p class="text-body-1 mb-0">
|
||||
Show
|
||||
</p>
|
||||
<AppSelect
|
||||
:model-value="itemsPerPage"
|
||||
:items="[
|
||||
{ value: 5, title: '5' },
|
||||
{ value: 25, title: '25' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: 100, title: '100' },
|
||||
{ value: -1, title: 'All' },
|
||||
]"
|
||||
style="inline-size: 5.5rem;"
|
||||
@update:model-value="itemsPerPage = parseInt($event, 10)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-4 flex-wrap">
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
placeholder="Search Permission"
|
||||
style="inline-size: 15.625rem;"
|
||||
/>
|
||||
<VBtn
|
||||
density="default"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isAddPermissionDialogVisible = true"
|
||||
>
|
||||
Add Permission
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
: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"
|
||||
>
|
||||
<!-- Name -->
|
||||
<template #item.name="{ item }">
|
||||
<div class="text-high-emphasis text-body-1">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Assigned To -->
|
||||
<template #item.assignedTo="{ item }">
|
||||
<div class="d-flex gap-4">
|
||||
<VChip
|
||||
v-for="text in item.assignedTo"
|
||||
:key="text"
|
||||
label
|
||||
size="small"
|
||||
:color="colors[text].color"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
{{ colors[text].text }}
|
||||
</VChip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalPermissions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
color="medium-emphasis"
|
||||
variant="text"
|
||||
@click="editPermission(item.name)"
|
||||
>
|
||||
<VIcon
|
||||
size="22"
|
||||
icon="tabler-edit"
|
||||
/>
|
||||
</VBtn>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="tabler-dots-vertical"
|
||||
size="22"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
|
||||
<AddEditPermissionDialog
|
||||
v-model:is-dialog-visible="isPermissionDialogVisible"
|
||||
v-model:permission-name="permissionName"
|
||||
/>
|
||||
<AddEditPermissionDialog v-model:is-dialog-visible="isAddPermissionDialogVisible" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
36
resources/js/pages/apps/roles/index.vue
Normal file
36
resources/js/pages/apps/roles/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import RoleCards from '@/views/apps/roles/RoleCards.vue'
|
||||
import UserList from '@/views/apps/roles/UserList.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<h4 class="text-h4 mb-1">
|
||||
Roles List
|
||||
</h4>
|
||||
<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">
|
||||
<h4 class="text-h4 mb-1 mt-6">
|
||||
Total users with their roles
|
||||
</h4>
|
||||
<p class="text-body-1 mb-0">
|
||||
Find all of your company’s administrator accounts and their associate roles.
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<!-- 👉 User List -->
|
||||
<UserList />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
525
resources/js/pages/apps/user/list/index.vue
Normal file
525
resources/js/pages/apps/user/list/index.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<script setup>
|
||||
import AddNewUserDrawer from '@/views/apps/user/list/AddNewUserDrawer.vue'
|
||||
|
||||
// 👉 Store
|
||||
const searchQuery = ref('')
|
||||
const selectedRole = ref()
|
||||
const selectedPlan = ref()
|
||||
const selectedStatus = ref()
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
const selectedRows = ref([])
|
||||
|
||||
const updateOptions = options => {
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'User',
|
||||
key: 'user',
|
||||
},
|
||||
{
|
||||
title: 'Role',
|
||||
key: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Plan',
|
||||
key: 'plan',
|
||||
},
|
||||
{
|
||||
title: 'Billing',
|
||||
key: 'billing',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
data: usersData,
|
||||
execute: fetchUsers,
|
||||
} = await useApi(createUrl('/apps/users', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
plan: selectedPlan,
|
||||
role: selectedRole,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const users = computed(() => usersData.value.users)
|
||||
const totalUsers = computed(() => usersData.value.totalUsers)
|
||||
|
||||
// 👉 search filters
|
||||
const roles = [
|
||||
{
|
||||
title: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
title: 'Author',
|
||||
value: 'author',
|
||||
},
|
||||
{
|
||||
title: 'Editor',
|
||||
value: 'editor',
|
||||
},
|
||||
{
|
||||
title: 'Maintainer',
|
||||
value: 'maintainer',
|
||||
},
|
||||
{
|
||||
title: 'Subscriber',
|
||||
value: 'subscriber',
|
||||
},
|
||||
]
|
||||
|
||||
const 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: 'tabler-user',
|
||||
}
|
||||
if (roleLowerCase === 'author')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'tabler-device-desktop',
|
||||
}
|
||||
if (roleLowerCase === 'maintainer')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'tabler-chart-pie',
|
||||
}
|
||||
if (roleLowerCase === 'editor')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'tabler-edit',
|
||||
}
|
||||
if (roleLowerCase === 'admin')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'tabler-crown',
|
||||
}
|
||||
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'tabler-user',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveUserStatusVariant = stat => {
|
||||
const statLowerCase = stat.toLowerCase()
|
||||
if (statLowerCase === 'pending')
|
||||
return 'warning'
|
||||
if (statLowerCase === 'active')
|
||||
return 'success'
|
||||
if (statLowerCase === 'inactive')
|
||||
return 'secondary'
|
||||
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
const isAddNewUserDrawerVisible = ref(false)
|
||||
|
||||
const addNewUser = async userData => {
|
||||
await $api('/apps/users', {
|
||||
method: 'POST',
|
||||
body: userData,
|
||||
})
|
||||
|
||||
// Refetch User
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
const deleteUser = async id => {
|
||||
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
|
||||
|
||||
// Delete from selectedRows
|
||||
const index = selectedRows.value.findIndex(row => row === id)
|
||||
if (index !== -1)
|
||||
selectedRows.value.splice(index, 1)
|
||||
|
||||
// Refetch User
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Session',
|
||||
value: '21,459',
|
||||
change: 29,
|
||||
desc: 'Total Users',
|
||||
icon: 'tabler-users',
|
||||
iconColor: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Paid Users',
|
||||
value: '4,567',
|
||||
change: 18,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'tabler-user-plus',
|
||||
iconColor: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Active Users',
|
||||
value: '19,860',
|
||||
change: -14,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'tabler-user-check',
|
||||
iconColor: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Pending Users',
|
||||
value: '237',
|
||||
change: 42,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'tabler-user-search',
|
||||
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">
|
||||
<div class="text-body-1 text-high-emphasis">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
<div class="d-flex gap-x-2 align-center">
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
<div
|
||||
class="text-base"
|
||||
:class="data.change > 0 ? 'text-success' : 'text-error'"
|
||||
>
|
||||
({{ prefixWithPlus(data.change) }}%)
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ data.desc }}
|
||||
</div>
|
||||
</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 class="mb-6">
|
||||
<VCardItem class="pb-4">
|
||||
<VCardTitle>Filters</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 👉 Select Role -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedRole"
|
||||
placeholder="Select Role"
|
||||
:items="roles"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- 👉 Select Plan -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedPlan"
|
||||
placeholder="Select Plan"
|
||||
:items="plans"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- 👉 Select Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Select Status"
|
||||
:items="status"
|
||||
clearable
|
||||
clear-icon="tabler-x"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<div class="me-3 d-flex gap-3">
|
||||
<AppSelect
|
||||
:model-value="itemsPerPage"
|
||||
:items="[
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 25, title: '25' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: 100, title: '100' },
|
||||
{ value: -1, title: 'All' },
|
||||
]"
|
||||
style="inline-size: 6.25rem;"
|
||||
@update:model-value="itemsPerPage = parseInt($event, 10)"
|
||||
/>
|
||||
</div>
|
||||
<VSpacer />
|
||||
|
||||
<div class="app-user-search-filter d-flex align-center flex-wrap gap-4">
|
||||
<!-- 👉 Search -->
|
||||
<div style="inline-size: 15.625rem;">
|
||||
<AppTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search User"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Export button -->
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="tabler-upload"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Add user button -->
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isAddNewUserDrawerVisible = true"
|
||||
>
|
||||
Add New User
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- SECTION datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:model-value="selectedRows"
|
||||
v-model:page="page"
|
||||
:items="users"
|
||||
item-value="id"
|
||||
:items-length="totalUsers"
|
||||
:headers="headers"
|
||||
class="text-no-wrap"
|
||||
show-select
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- User -->
|
||||
<template #item.user="{ item }">
|
||||
<div class="d-flex align-center gap-x-4">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:variant="!item.avatar ? 'tonal' : undefined"
|
||||
:color="!item.avatar ? resolveUserRoleVariant(item.role).color : undefined"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.fullName) }}</span>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<h6 class="text-base">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
|
||||
class="font-weight-medium text-link"
|
||||
>
|
||||
{{ item.fullName }}
|
||||
</RouterLink>
|
||||
</h6>
|
||||
<div class="text-sm">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 👉 Role -->
|
||||
<template #item.role="{ item }">
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<VIcon
|
||||
:size="22"
|
||||
:icon="resolveUserRoleVariant(item.role).icon"
|
||||
:color="resolveUserRoleVariant(item.role).color"
|
||||
/>
|
||||
|
||||
<div class="text-capitalize text-high-emphasis text-body-1">
|
||||
{{ item.role }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Plan -->
|
||||
<template #item.plan="{ item }">
|
||||
<div class="text-body-1 text-high-emphasis text-capitalize">
|
||||
{{ item.currentPlan }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="resolveUserStatusVariant(item.status)"
|
||||
size="small"
|
||||
label
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn @click="deleteUser(item.id)">
|
||||
<VIcon icon="tabler-trash" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn>
|
||||
<VIcon icon="tabler-eye" />
|
||||
</IconBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
variant="text"
|
||||
color="medium-emphasis"
|
||||
>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem :to="{ name: 'apps-user-view-id', params: { id: item.id } }">
|
||||
<template #prepend>
|
||||
<VIcon icon="tabler-eye" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>View</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem link>
|
||||
<template #prepend>
|
||||
<VIcon icon="tabler-pencil" />
|
||||
</template>
|
||||
<VListItemTitle>Edit</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem @click="deleteUser(item.id)">
|
||||
<template #prepend>
|
||||
<VIcon icon="tabler-trash" />
|
||||
</template>
|
||||
<VListItemTitle>Delete</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- pagination -->
|
||||
<template #bottom>
|
||||
<TablePagination
|
||||
v-model:page="page"
|
||||
:items-per-page="itemsPerPage"
|
||||
:total-items="totalUsers"
|
||||
/>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
<!-- SECTION -->
|
||||
</VCard>
|
||||
<!-- 👉 Add New User -->
|
||||
<AddNewUserDrawer
|
||||
v-model:is-drawer-open="isAddNewUserDrawerVisible"
|
||||
@user-data="addNewUser"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
105
resources/js/pages/apps/user/view/[id].vue
Normal file
105
resources/js/pages/apps/user/view/[id].vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import UserBioPanel from '@/views/apps/user/view/UserBioPanel.vue'
|
||||
import UserTabAccount from '@/views/apps/user/view/UserTabAccount.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 UserTabSecurity from '@/views/apps/user/view/UserTabSecurity.vue'
|
||||
|
||||
const route = useRoute('apps-user-view-id')
|
||||
const userTab = ref(null)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
icon: 'tabler-users',
|
||||
title: 'Account',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-lock',
|
||||
title: 'Security',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-bookmark',
|
||||
title: 'Billing & Plan',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-bell',
|
||||
title: 'Notifications',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-link',
|
||||
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
|
||||
:size="18"
|
||||
:icon="tab.icon"
|
||||
class="me-1"
|
||||
/>
|
||||
<span>{{ tab.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow
|
||||
v-model="userTab"
|
||||
class="mt-6 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem>
|
||||
<UserTabAccount />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabSecurity />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabBillingsPlans />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabNotifications />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabConnections />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<div v-else>
|
||||
<VAlert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
Invoice with ID {{ route.params.id }} not found!
|
||||
</VAlert>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user