initial commit
This commit is contained in:
308
resources/js/pages/apps/academy/course-details.vue
Normal file
308
resources/js/pages/apps/academy/course-details.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup>
|
||||
import { VideoPlayer } from '@videojs-player/vue'
|
||||
import instructorPosterImage from '@images/pages/instructor-poster-image.png'
|
||||
import 'video.js/dist/video-js.css'
|
||||
|
||||
const courseDetails = ref()
|
||||
const { data, error } = await useApi('/apps/academy/course-details')
|
||||
if (error.value)
|
||||
console.log(error.value)
|
||||
else if (data.value)
|
||||
courseDetails.value = data.value
|
||||
const panelStatus = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem
|
||||
title="UI/UX Basic Fundamentals"
|
||||
class="pb-6"
|
||||
>
|
||||
<template #subtitle>
|
||||
<div class="text-body-1">
|
||||
Prof. <span class="text-h6 d-inline-block">{{ courseDetails?.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<div class="d-flex gap-4 align-center">
|
||||
<VChip
|
||||
variant="tonal"
|
||||
color="error"
|
||||
size="small"
|
||||
>
|
||||
UI/UX
|
||||
</VChip>
|
||||
<VIcon
|
||||
size="20"
|
||||
class="cursor-pointer"
|
||||
icon="ri-share-forward-line"
|
||||
/>
|
||||
<VIcon
|
||||
size="20"
|
||||
class="cursor-pointer"
|
||||
icon="ri-bookmark-line"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
>
|
||||
<div class="px-2 pt-2">
|
||||
<VideoPlayer
|
||||
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
|
||||
:poster="instructorPosterImage"
|
||||
controls
|
||||
plays-inline
|
||||
:height="$vuetify.display.mdAndUp ? 440 : 250"
|
||||
class="w-100 rounded"
|
||||
/>
|
||||
</div>
|
||||
<VCardText>
|
||||
<h5 class="text-h5 mb-4">
|
||||
About this course
|
||||
</h5>
|
||||
<p class="text-body-1">
|
||||
{{ courseDetails?.about }}
|
||||
</p>
|
||||
<VDivider class="my-6" />
|
||||
|
||||
<h5 class="text-h5 mb-4">
|
||||
By the numbers
|
||||
</h5>
|
||||
<div class="d-flex gap-x-12 gap-y-5 flex-wrap">
|
||||
<div>
|
||||
<VList class="card-list text-medium-emphasis">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-check-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Skill Level: {{ courseDetails?.skillLevel }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-user-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Students: {{ courseDetails?.totalStudents }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-global-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Languages: {{ courseDetails?.language }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-closed-captioning-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Captions: {{ courseDetails?.isCaptions }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VList class="card-list text-medium-emphasis">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-pencil-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Lectures: {{ courseDetails?.totalLectures }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
icon="ri-time-line"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>Video: {{ courseDetails?.length }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</div>
|
||||
</div>
|
||||
<VDivider class="my-6" />
|
||||
|
||||
<h5 class="text-h5 mb-4">
|
||||
Description
|
||||
</h5>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="courseDetails?.description" />
|
||||
|
||||
<VDivider class="my-6" />
|
||||
|
||||
<h5 class="text-h5 mb-4">
|
||||
Instructor
|
||||
</h5>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
:image="courseDetails?.instructorAvatar"
|
||||
size="38"
|
||||
class="me-3"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ courseDetails?.instructor }}
|
||||
</h6>
|
||||
<div class="text-body-1">
|
||||
{{ courseDetails?.instructorPosition }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<div class="course-content">
|
||||
<VExpansionPanels
|
||||
v-model="panelStatus"
|
||||
variant="accordion"
|
||||
>
|
||||
<VExpansionPanel
|
||||
v-for="(section, index) in courseDetails?.content"
|
||||
:key="index"
|
||||
elevation="0"
|
||||
collapse-icon="ri-arrow-down-s-line"
|
||||
:expand-icon="$vuetify.locale.isRtl ? 'ri-arrow-left-s-line' : 'ri-arrow-right-s-line'"
|
||||
:value="index"
|
||||
>
|
||||
<template #title>
|
||||
<div>
|
||||
<h5 class="text-h5">
|
||||
{{ section.title }}
|
||||
</h5>
|
||||
<div class="text-body-1">
|
||||
{{ section.status }} | {{ section.time }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="(topic, id) in section.topics"
|
||||
:key="id"
|
||||
class="py-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<VCheckbox
|
||||
class="ps-3 me-3"
|
||||
:model-value="topic.isCompleted"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<h6 class="text-h6">
|
||||
{{ topic.title }}
|
||||
</h6>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
<div class="text-body-2">
|
||||
{{ topic.time }}
|
||||
</div>
|
||||
</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</template>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.course-content {
|
||||
position: sticky;
|
||||
inset-block: 4rem 0;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
--v-card-list-gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.course-content{
|
||||
.v-expansion-panels{
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 6px;
|
||||
|
||||
.v-expansion-panel{
|
||||
&--active{
|
||||
.v-expansion-panel-title--active{
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
|
||||
.v-expansion-panel-title__overlay{
|
||||
opacity: var(--v-hover-opacity) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-expansion-panel-title{
|
||||
.v-expansion-panel-title__overlay{
|
||||
background-color: rgba(var(--v-theme-on-surface));
|
||||
opacity: var(--v-hover-opacity);
|
||||
|
||||
}
|
||||
|
||||
&:hover{
|
||||
.v-expansion-panel-title__overlay{
|
||||
opacity: var(--v-hover-opacity) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon{
|
||||
.v-icon{
|
||||
block-size: 1.5rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 1.5rem;
|
||||
inline-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-expansion-panel-text{
|
||||
&__wrapper{
|
||||
padding-block: 1rem;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-list {
|
||||
.v-list-item__prepend{
|
||||
.v-list-item__spacer{
|
||||
inline-size: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
356
resources/js/pages/apps/academy/dashboard.vue
Normal file
356
resources/js/pages/apps/academy/dashboard.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<script setup>
|
||||
import AcademyAssignmentProgress from '@/views/apps/academy/AcademyAssignmentProgress.vue'
|
||||
import AcademyCardInterestedTopics from '@/views/apps/academy/AcademyCardInterestedTopics.vue'
|
||||
import AcademyCardPopularInstructors from '@/views/apps/academy/AcademyCardPopularInstructors.vue'
|
||||
import AcademyCardTopCourses from '@/views/apps/academy/AcademyCardTopCourses.vue'
|
||||
import AcademyCourseTable from '@/views/apps/academy/AcademyCourseTable.vue'
|
||||
import AcademyUpcomingWebinar from '@/views/apps/academy/AcademyUpcomingWebinar.vue'
|
||||
import customCheck from '@images/svg/check.svg'
|
||||
import customLaptop from '@images/svg/laptop.svg'
|
||||
import customLightbulb from '@images/svg/lightbulb.svg'
|
||||
|
||||
const borderColor = 'rgba(var(--v-border-color), var(--v-border-opacity))'
|
||||
|
||||
const topicsChartConfig = {
|
||||
chart: {
|
||||
height: 270,
|
||||
type: 'bar',
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: '70%',
|
||||
distributed: true,
|
||||
borderRadius: 7,
|
||||
},
|
||||
},
|
||||
colors: [
|
||||
'#8C57FF',
|
||||
'#16B1FF',
|
||||
'#56CA00',
|
||||
'#8A8D93',
|
||||
'#FF4C51',
|
||||
'#FFB400',
|
||||
],
|
||||
grid: {
|
||||
borderColor,
|
||||
strokeDashArray: 10,
|
||||
xaxis: { lines: { show: true } },
|
||||
yaxis: { lines: { show: false } },
|
||||
padding: {
|
||||
top: -35,
|
||||
bottom: -12,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
colors: ['#fff'],
|
||||
fontWeight: 200,
|
||||
fontSize: '13px',
|
||||
},
|
||||
offsetX: 0,
|
||||
dropShadow: { enabled: false },
|
||||
formatter(val, opt) {
|
||||
return topicsChartConfig.labels[opt.dataPointIndex]
|
||||
},
|
||||
},
|
||||
labels: [
|
||||
'UI Design',
|
||||
'UX Design',
|
||||
'Music',
|
||||
'Animation',
|
||||
'Vue',
|
||||
'SEO',
|
||||
],
|
||||
xaxis: {
|
||||
categories: [
|
||||
'6',
|
||||
'5',
|
||||
'4',
|
||||
'3',
|
||||
'2',
|
||||
'1',
|
||||
],
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { show: false },
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
|
||||
fontSize: '13px',
|
||||
},
|
||||
formatter(val) {
|
||||
return `${ val }%`
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
max: 35,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
|
||||
fontSize: '13px',
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
style: { fontSize: '12px' },
|
||||
onDatasetHover: { highlightDataSeries: false },
|
||||
},
|
||||
legend: { show: false },
|
||||
}
|
||||
|
||||
const donutChartColors = {
|
||||
donut: {
|
||||
series1: '#22A95E',
|
||||
series2: '#24B364',
|
||||
series3: '#56CA00',
|
||||
series4: '#53D28C',
|
||||
series5: '#7EDDA9',
|
||||
series6: '#A9E9C5',
|
||||
},
|
||||
}
|
||||
|
||||
const timeSpendingChartConfig = {
|
||||
chart: {
|
||||
height: 157,
|
||||
width: 130,
|
||||
parentHeightOffset: 0,
|
||||
type: 'donut',
|
||||
},
|
||||
labels: [
|
||||
'36h',
|
||||
'56h',
|
||||
'16h',
|
||||
'32h',
|
||||
'56h',
|
||||
'16h',
|
||||
],
|
||||
colors: [
|
||||
donutChartColors.donut.series1,
|
||||
donutChartColors.donut.series2,
|
||||
donutChartColors.donut.series3,
|
||||
donutChartColors.donut.series4,
|
||||
donutChartColors.donut.series5,
|
||||
donutChartColors.donut.series6,
|
||||
],
|
||||
stroke: { width: 0 },
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
formatter(val) {
|
||||
return `${ Number.parseInt(val) }%`
|
||||
},
|
||||
},
|
||||
legend: { show: false },
|
||||
tooltip: { theme: false },
|
||||
grid: { padding: { top: 0 } },
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '75%',
|
||||
labels: {
|
||||
show: true,
|
||||
value: {
|
||||
fontSize: '1.125rem',
|
||||
color: 'rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))',
|
||||
fontWeight: 500,
|
||||
offsetY: -15,
|
||||
formatter(val) {
|
||||
return `${ Number.parseInt(val) }%`
|
||||
},
|
||||
},
|
||||
name: { offsetY: 20 },
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: '.7rem',
|
||||
label: 'Total',
|
||||
color: 'rgba(var(--v-theme-on-background), var(--v-disabled-opacity))',
|
||||
formatter() {
|
||||
return '231h'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const timeSpendingChartSeries = [
|
||||
23,
|
||||
35,
|
||||
10,
|
||||
20,
|
||||
35,
|
||||
23,
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow class="py-5 match-height">
|
||||
<!-- 👉 Welcome -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
sm="6"
|
||||
:class="$vuetify.display.smAndUp ? 'border-e' : 'border-b'"
|
||||
>
|
||||
<div class="pe-3">
|
||||
<h5 class="text-h5 text-high-emphasis mb-1">
|
||||
Welcome back, Felecia 👋🏻
|
||||
</h5>
|
||||
|
||||
<div
|
||||
class="text-wrap text-medium-emphasis mb-4"
|
||||
style="max-inline-size: 400px;"
|
||||
>
|
||||
Your progress this week is Awesome. let's keep it up
|
||||
and get a lot of points reward!
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-space-between flex-wrap gap-6 flex-column flex-md-row">
|
||||
<div
|
||||
v-for="{ title, value, icon, color } in [
|
||||
{ title: 'Hours Spent', value: '34h', icon: customLaptop, color: 'primary' },
|
||||
{ title: 'Test Results', value: '82%', icon: customLightbulb, color: 'info' },
|
||||
{ title: 'Course Completed', value: '14', icon: customCheck, color: 'warning' },
|
||||
]"
|
||||
:key="title"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
:color="color"
|
||||
rounded
|
||||
size="54"
|
||||
class="text-primary me-4"
|
||||
>
|
||||
<VIcon
|
||||
:icon="icon"
|
||||
size="38"
|
||||
/>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<h6 class="text-h6 text-medium-emphasis">
|
||||
{{ title }}
|
||||
</h6>
|
||||
<h4
|
||||
class="text-h4 font-weight-medium"
|
||||
:class="`text-${color}`"
|
||||
>
|
||||
{{ value }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Time Spending -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex flex-column ps-3">
|
||||
<h5 class="text-h5 mb-1 text-no-wrap">
|
||||
Time Spending
|
||||
</h5>
|
||||
<div class="mb-6 text-body-1">
|
||||
Weekly Report
|
||||
</div>
|
||||
<h4 class="text-h4 mb-2">
|
||||
231<span class="text-medium-emphasis">h</span> 14<span class="text-medium-emphasis">m</span>
|
||||
</h4>
|
||||
<div>
|
||||
<VChip
|
||||
color="success"
|
||||
density="comfortable"
|
||||
>
|
||||
+18.4%
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<VueApexCharts
|
||||
type="donut"
|
||||
height="150"
|
||||
width="150"
|
||||
:options="timeSpendingChartConfig"
|
||||
:series="timeSpendingChartSeries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<!-- 👉 Topic You are Interested in -->
|
||||
<AcademyCardInterestedTopics />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Popular Instructors -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AcademyCardPopularInstructors />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Academy Top Courses -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AcademyCardTopCourses />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Academy Upcoming Webinar -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AcademyUpcomingWebinar />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Academy Assignment Progress -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AcademyAssignmentProgress />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Academy Course Table -->
|
||||
<VCol>
|
||||
<AcademyCourseTable />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
|
||||
.topicCard{
|
||||
.v-badge.v-badge--dot{
|
||||
.v-badge__badge{
|
||||
border-radius: 6px;
|
||||
block-size: 12px;
|
||||
inline-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
229
resources/js/pages/apps/academy/my-course.vue
Normal file
229
resources/js/pages/apps/academy/my-course.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script setup>
|
||||
import { VideoPlayer } from '@videojs-player/vue'
|
||||
import AcademyMyCourses from '@/views/apps/academy/AcademyMyCourses.vue'
|
||||
import academyCourseIllustration1 from '@images/pages/academy-course-illustration1.png'
|
||||
import academyCourseIllustration2 from '@images/pages/academy-course-illustration2.png'
|
||||
import boyAcademyIllustration from '@images/pages/boy-academy-illustration.png'
|
||||
import girlAcademyIllustration from '@images/pages/girl-academy-illustration.png'
|
||||
import guitarCoursePoster from '@images/pages/guitar-course.png'
|
||||
import singingCoursePoster from '@images/pages/singing-course.png'
|
||||
|
||||
const searchQuery = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="py-12 position-relative">
|
||||
<div
|
||||
class="d-flex flex-column gap-y-4 mx-auto"
|
||||
:class="$vuetify.display.mdAndUp ? 'w-50' : 'w-100'"
|
||||
>
|
||||
<h4
|
||||
class="text-h4 text-center text-wrap mx-auto"
|
||||
:class="$vuetify.display.mdAndUp ? 'w-75' : 'w-100'"
|
||||
>
|
||||
Education, talents, and career
|
||||
opportunities. <span class="text-primary"> All in one place.</span>
|
||||
</h4>
|
||||
<p class="text-center text-wrap text-body-1 mx-auto mb-0">
|
||||
Grow your skill with the most reliable online courses and certifications in marketing, information technology, programming, and data science.
|
||||
</p>
|
||||
<div class="d-flex justify-center align-center gap-x-4">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
density="compact"
|
||||
placeholder="Find your course"
|
||||
style="max-inline-size: 400px;"
|
||||
/>
|
||||
<VBtn
|
||||
color="primary"
|
||||
icon="ri-search-line"
|
||||
class="rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
:src="academyCourseIllustration1"
|
||||
class="illustration1 d-none d-md-block"
|
||||
height="180"
|
||||
>
|
||||
<img
|
||||
:src="academyCourseIllustration2"
|
||||
class="illustration2 d-none d-md-block"
|
||||
height="100"
|
||||
>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<AcademyMyCourses :search-query="searchQuery" />
|
||||
|
||||
<div class="mb-6">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
color="rgba(var(--v-theme-primary), 0.08)"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column-reverse flex-sm-row gap-4 justify-space-between">
|
||||
<div class="text-center text-sm-start">
|
||||
<h5 class="text-h5 text-primary mb-1">
|
||||
Earn a Certificate
|
||||
</h5>
|
||||
<p class="text-body-1 text-high-emphasis mb-6">
|
||||
Get the right professional certificate program for you.
|
||||
</p>
|
||||
<VBtn>View Programs</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<img :src="boyAcademyIllustration">
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
color="rgba(var(--v-theme-error), 0.08)"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column-reverse flex-sm-row gap-4 justify-space-between">
|
||||
<div class="text-center text-sm-start">
|
||||
<h5 class="text-h5 text-error mb-1">
|
||||
Best Rated Courses
|
||||
</h5>
|
||||
<p class="text-body-1 text-high-emphasis text-wrap mb-6">
|
||||
Enroll now in the most popular and best rated courses.
|
||||
</p>
|
||||
<VBtn color="error">
|
||||
View Courses
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<img :src="girlAcademyIllustration">
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<div class="d-flex flex-column align-center gap-y-4 h-100 justify-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
size="52"
|
||||
rounded
|
||||
color="primary"
|
||||
>
|
||||
<VIcon
|
||||
icon="ri-gift-line"
|
||||
size="36"
|
||||
/>
|
||||
</VAvatar>
|
||||
<h4 class="text-h4">
|
||||
Today's Free Courses
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-0">
|
||||
We offers 284 Free Online courses from top tutors and companies to help you start or advance your career skills. Learn online for free and fast today!
|
||||
</p>
|
||||
<VBtn>Get Premium Courses</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
>
|
||||
<div class="pa-2">
|
||||
<VideoPlayer
|
||||
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
|
||||
:poster="singingCoursePoster"
|
||||
controls
|
||||
plays-inline
|
||||
:height="$vuetify.display.mdAndUp ? 200 : 150"
|
||||
class="w-100 rounded"
|
||||
/>
|
||||
</div>
|
||||
<VCardText class="pt-3">
|
||||
<h5 class="text-h5 mb-2">
|
||||
Your First Singing Lesson
|
||||
</h5>
|
||||
<p class="text-body-1 mb-0">
|
||||
In the same way as any other artistic domain, singing lends itself perfectly to self-teaching.
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
>
|
||||
<div class="pa-2">
|
||||
<VideoPlayer
|
||||
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
|
||||
:poster="guitarCoursePoster"
|
||||
controls
|
||||
plays-inline
|
||||
:height="$vuetify.display.mdAndUp ? 200 : 150"
|
||||
class="w-100 rounded"
|
||||
/>
|
||||
</div>
|
||||
<VCardText class="pt-3">
|
||||
<h5 class="text-h5 mb-2">
|
||||
Guitar for Beginners
|
||||
</h5>
|
||||
<p class="text-body-1 mb-0">
|
||||
The Fender Acoustic Guitar is best choice for beginners and professionals.
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'video.js/dist/video-js.css';
|
||||
|
||||
.illustration1 {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.illustration2 {
|
||||
position: absolute;
|
||||
inset-block-start: 2rem;
|
||||
inset-inline-start: 2.5rem;
|
||||
}
|
||||
</style>
|
175
resources/js/pages/apps/calendar.vue
Normal file
175
resources/js/pages/apps/calendar.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script setup>
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import {
|
||||
blankEvent,
|
||||
useCalendar,
|
||||
} from '@/views/apps/calendar/useCalendar'
|
||||
import { useCalendarStore } from '@/views/apps/calendar/useCalendarStore'
|
||||
|
||||
// Components
|
||||
import CalendarEventHandler from '@/views/apps/calendar/CalendarEventHandler.vue'
|
||||
|
||||
// 👉 Store
|
||||
const store = useCalendarStore()
|
||||
|
||||
// 👉 Event
|
||||
const event = ref(structuredClone(blankEvent))
|
||||
const isEventHandlerSidebarActive = ref(false)
|
||||
|
||||
watch(isEventHandlerSidebarActive, val => {
|
||||
if (!val)
|
||||
event.value = structuredClone(blankEvent)
|
||||
})
|
||||
|
||||
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
|
||||
|
||||
// 👉 useCalendar
|
||||
const { refCalendar, calendarOptions, addEvent, updateEvent, removeEvent, jumpToDate } = useCalendar(event, isEventHandlerSidebarActive, isLeftSidebarOpen)
|
||||
|
||||
// SECTION Sidebar
|
||||
|
||||
// 👉 Check all
|
||||
const checkAll = computed({
|
||||
|
||||
/*GET: Return boolean `true` => if length of options matches length of selected filters => Length matches when all events are selected
|
||||
SET: If value is `true` => then add all available options in selected filters => Select All
|
||||
Else if => all filters are selected (by checking length of both array) => Empty Selected array => Deselect All
|
||||
*/
|
||||
get: () => store.selectedCalendars.length === store.availableCalendars.length,
|
||||
set: val => {
|
||||
if (val)
|
||||
store.selectedCalendars = store.availableCalendars.map(i => i.label)
|
||||
else if (store.selectedCalendars.length === store.availableCalendars.length)
|
||||
store.selectedCalendars = []
|
||||
},
|
||||
})
|
||||
|
||||
// !SECTION
|
||||
const calendarApi = ref(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<!-- `z-index: 0` Allows overlapping vertical nav on calendar -->
|
||||
<VLayout style="z-index: 0;">
|
||||
<!-- 👉 Navigation drawer -->
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
width="292"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
class="calendar-add-event-drawer"
|
||||
:temporary="$vuetify.display.mdAndDown"
|
||||
>
|
||||
<div class="pa-5">
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="ri-add-line"
|
||||
@click="isEventHandlerSidebarActive = true"
|
||||
>
|
||||
Add event
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex align-center justify-center pa-2">
|
||||
<AppDateTimePicker
|
||||
v-model="calendarApi"
|
||||
:config="{ inline: true }"
|
||||
class="calendar-date-picker"
|
||||
@update:model-value="jumpToDate($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<div class="pa-5">
|
||||
<h5 class="text-h5 mb-4">
|
||||
Event Filters
|
||||
</h5>
|
||||
|
||||
<div class="d-flex flex-column calendars-checkbox">
|
||||
<VCheckbox
|
||||
v-model="checkAll"
|
||||
label="View all"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-for="calendar in store.availableCalendars"
|
||||
:key="calendar.label"
|
||||
v-model="store.selectedCalendars"
|
||||
:value="calendar.label"
|
||||
:color="calendar.color"
|
||||
:label="calendar.label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VNavigationDrawer>
|
||||
|
||||
<VMain>
|
||||
<VCard flat>
|
||||
<FullCalendar
|
||||
ref="refCalendar"
|
||||
:options="calendarOptions"
|
||||
/>
|
||||
</VCard>
|
||||
</VMain>
|
||||
</VLayout>
|
||||
</VCard>
|
||||
<CalendarEventHandler
|
||||
v-model:isDrawerOpen="isEventHandlerSidebarActive"
|
||||
:event="event"
|
||||
@add-event="addEvent"
|
||||
@update-event="updateEvent"
|
||||
@remove-event="removeEvent"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/full-calendar";
|
||||
|
||||
.calendars-checkbox {
|
||||
.v-label {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
opacity: var(--v-high-emphasis-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-add-event-drawer {
|
||||
&.v-navigation-drawer:not(.v-navigation-drawer--temporary) {
|
||||
border-end-start-radius: 0.375rem;
|
||||
border-start-start-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-date-picker {
|
||||
display: none;
|
||||
|
||||
+.flatpickr-input {
|
||||
+.flatpickr-calendar.inline {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
.flatpickr-months {
|
||||
border-block-end: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .flatpickr-calendar .flatpickr-weekdays {
|
||||
margin-block: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-layout {
|
||||
overflow: visible !important;
|
||||
|
||||
.v-card {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
429
resources/js/pages/apps/chat.vue
Normal file
429
resources/js/pages/apps/chat.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import {
|
||||
useDisplay,
|
||||
useTheme,
|
||||
} from 'vuetify'
|
||||
import { themes } from '@/plugins/vuetify/theme'
|
||||
import ChatActiveChatUserProfileSidebarContent from '@/views/apps/chat/ChatActiveChatUserProfileSidebarContent.vue'
|
||||
import ChatLeftSidebarContent from '@/views/apps/chat/ChatLeftSidebarContent.vue'
|
||||
import ChatLog from '@/views/apps/chat/ChatLog.vue'
|
||||
import ChatUserProfileSidebarContent from '@/views/apps/chat/ChatUserProfileSidebarContent.vue'
|
||||
import { useChat } from '@/views/apps/chat/useChat'
|
||||
import { useChatStore } from '@/views/apps/chat/useChatStore'
|
||||
|
||||
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
|
||||
|
||||
// composables
|
||||
const vuetifyDisplays = useDisplay()
|
||||
const store = useChatStore()
|
||||
const { isLeftSidebarOpen } = useResponsiveLeftSidebar(vuetifyDisplays.smAndDown)
|
||||
const { resolveAvatarBadgeVariant } = useChat()
|
||||
|
||||
// Perfect scrollbar
|
||||
const chatLogPS = ref()
|
||||
|
||||
const scrollToBottomInChatLog = () => {
|
||||
const scrollEl = chatLogPS.value.$el || chatLogPS.value
|
||||
|
||||
scrollEl.scrollTop = scrollEl.scrollHeight
|
||||
}
|
||||
|
||||
// Search query
|
||||
const q = ref('')
|
||||
|
||||
watch(q, val => store.fetchChatsAndContacts(val), { immediate: true })
|
||||
|
||||
// Open Sidebar in smAndDown when "start conversation" is clicked
|
||||
const startConversation = () => {
|
||||
if (vuetifyDisplays.mdAndUp.value)
|
||||
return
|
||||
isLeftSidebarOpen.value = true
|
||||
}
|
||||
|
||||
// Chat message
|
||||
const msg = ref('')
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!msg.value)
|
||||
return
|
||||
await store.sendMsg(msg.value)
|
||||
|
||||
// Reset message input
|
||||
msg.value = ''
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
scrollToBottomInChatLog()
|
||||
})
|
||||
}
|
||||
|
||||
const openChatOfContact = async userId => {
|
||||
await store.getChat(userId)
|
||||
|
||||
// Reset message input
|
||||
msg.value = ''
|
||||
|
||||
// Set unseenMsgs to 0
|
||||
const contact = store.chatsContacts.find(c => c.id === userId)
|
||||
if (contact)
|
||||
contact.chat.unseenMsgs = 0
|
||||
|
||||
// if smAndDown => Close Chat & Contacts left sidebar
|
||||
if (vuetifyDisplays.smAndDown.value)
|
||||
isLeftSidebarOpen.value = false
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
scrollToBottomInChatLog()
|
||||
})
|
||||
}
|
||||
|
||||
// User profile sidebar
|
||||
const isUserProfileSidebarOpen = ref(false)
|
||||
|
||||
// Active chat user profile sidebar
|
||||
const isActiveChatUserProfileSidebarOpen = ref(false)
|
||||
|
||||
// file input
|
||||
const refInputEl = ref()
|
||||
|
||||
const moreList = [
|
||||
{
|
||||
title: 'View Contact',
|
||||
value: 'View Contact',
|
||||
},
|
||||
{
|
||||
title: 'Mute Notifications',
|
||||
value: 'Mute Notifications',
|
||||
},
|
||||
{
|
||||
title: 'Block Contact',
|
||||
value: 'Block Contact',
|
||||
},
|
||||
{
|
||||
title: 'Clear Chat',
|
||||
value: 'Clear Chat',
|
||||
},
|
||||
{
|
||||
title: 'Report',
|
||||
value: 'Report',
|
||||
},
|
||||
]
|
||||
|
||||
const { name } = useTheme()
|
||||
|
||||
const chatContentContainerBg = computed(() => {
|
||||
let color = 'transparent'
|
||||
if (themes)
|
||||
color = themes?.[name.value].colors?.['chat-bg']
|
||||
|
||||
return color
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLayout class="chat-app-layout bg-surface">
|
||||
<!-- 👉 user profile sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isUserProfileSidebarOpen"
|
||||
temporary
|
||||
touchless
|
||||
absolute
|
||||
class="user-profile-sidebar"
|
||||
location="start"
|
||||
width="370"
|
||||
>
|
||||
<ChatUserProfileSidebarContent @close="isUserProfileSidebarOpen = false" />
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Active Chat sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isActiveChatUserProfileSidebarOpen"
|
||||
width="374"
|
||||
absolute
|
||||
temporary
|
||||
location="end"
|
||||
touchless
|
||||
class="active-chat-user-profile-sidebar"
|
||||
>
|
||||
<ChatActiveChatUserProfileSidebarContent @close="isActiveChatUserProfileSidebarOpen = false" />
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Left sidebar -->
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
width="370"
|
||||
:temporary="$vuetify.display.smAndDown"
|
||||
class="chat-list-sidebar"
|
||||
:permanent="$vuetify.display.mdAndUp"
|
||||
>
|
||||
<ChatLeftSidebarContent
|
||||
v-model:isDrawerOpen="isLeftSidebarOpen"
|
||||
v-model:search="q"
|
||||
@open-chat-of-contact="openChatOfContact"
|
||||
@show-user-profile="isUserProfileSidebarOpen = true"
|
||||
@close="isLeftSidebarOpen = false"
|
||||
/>
|
||||
</VNavigationDrawer>
|
||||
|
||||
<!-- 👉 Chat content -->
|
||||
<VMain class="chat-content-container">
|
||||
<!-- 👉 Right content: Active Chat -->
|
||||
<div
|
||||
v-if="store.activeChat"
|
||||
class="d-flex flex-column h-100"
|
||||
>
|
||||
<!-- 👉 Active chat header -->
|
||||
<div class="active-chat-header d-flex align-center text-medium-emphasis">
|
||||
<!-- Sidebar toggler -->
|
||||
<IconBtn
|
||||
class="d-md-none me-4"
|
||||
@click="isLeftSidebarOpen = true"
|
||||
>
|
||||
<VIcon icon="ri-menu-line" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- avatar -->
|
||||
<div
|
||||
class="d-flex align-center cursor-pointer"
|
||||
@click="isActiveChatUserProfileSidebarOpen = true"
|
||||
>
|
||||
<VBadge
|
||||
dot
|
||||
location="bottom right"
|
||||
offset-x="3"
|
||||
offset-y="3"
|
||||
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
|
||||
bordered
|
||||
class="me-4"
|
||||
>
|
||||
<VAvatar
|
||||
size="40"
|
||||
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
|
||||
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<VImg
|
||||
v-if="store.activeChat.contact.avatar"
|
||||
:src="store.activeChat.contact.avatar"
|
||||
:alt="store.activeChat.contact.fullName"
|
||||
/>
|
||||
<span v-else>{{ avatarText(store.activeChat.contact.fullName) }}</span>
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ store.activeChat.contact.fullName }}
|
||||
</h6>
|
||||
<p class="text-body-2 text-truncate mb-0">
|
||||
{{ store.activeChat.contact.role }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- Header right content -->
|
||||
<div class="d-sm-flex align-center d-none">
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-phone-line" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-vidicon-line" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-search-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<MoreBtn :menu-list="moreList" />
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- Chat log -->
|
||||
<PerfectScrollbar
|
||||
ref="chatLogPS"
|
||||
tag="ul"
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<ChatLog />
|
||||
</PerfectScrollbar>
|
||||
|
||||
<!-- Message form -->
|
||||
<VForm
|
||||
class="chat-log-message-form mb-5 mx-5"
|
||||
@submit.prevent="sendMessage"
|
||||
>
|
||||
<VTextField
|
||||
:key="store.activeChat?.contact.id"
|
||||
v-model="msg"
|
||||
variant="solo"
|
||||
density="default"
|
||||
class="chat-message-input"
|
||||
placeholder="Type your message..."
|
||||
autofocus
|
||||
>
|
||||
<template #append-inner>
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-mic-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
class="me-4"
|
||||
@click="refInputEl?.click()"
|
||||
>
|
||||
<VIcon icon="ri-attachment-2" />
|
||||
</IconBtn>
|
||||
|
||||
<VBtn
|
||||
append-icon="ri-send-plane-line"
|
||||
@click="sendMessage"
|
||||
>
|
||||
Send
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
|
||||
<input
|
||||
ref="refInputEl"
|
||||
type="file"
|
||||
name="file"
|
||||
accept=".jpeg,.png,.jpg,GIF"
|
||||
hidden
|
||||
>
|
||||
</VForm>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Start conversation -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex h-100 align-center justify-center flex-column"
|
||||
>
|
||||
<VAvatar
|
||||
size="98"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mb-5"
|
||||
>
|
||||
<VIcon
|
||||
size="50"
|
||||
icon="ri-wechat-line"
|
||||
/>
|
||||
</VAvatar>
|
||||
<p
|
||||
class="mb-0 px-4 py-2 font-weight-medium elevation-2 rounded-xl bg-primary"
|
||||
:class="[{ 'cursor-pointer': $vuetify.display.smAndDown }]"
|
||||
@click="startConversation"
|
||||
>
|
||||
Start Conversation
|
||||
</p>
|
||||
</div>
|
||||
</VMain>
|
||||
</VLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/vuetify.scss";
|
||||
@use "@core-scss/base/mixins.scss";
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
// Variables
|
||||
$chat-app-header-height: 76px;
|
||||
|
||||
// Placeholders
|
||||
%chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-block-size: $chat-app-header-height;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
|
||||
.chat-app-layout {
|
||||
border-radius: vuetify.$card-border-radius;
|
||||
|
||||
@include mixins.elevation(vuetify.$card-elevation);
|
||||
|
||||
$sel-chat-app-layout: &;
|
||||
|
||||
@at-root {
|
||||
.skin--bordered {
|
||||
@include mixins.bordered-skin($sel-chat-app-layout);
|
||||
}
|
||||
}
|
||||
|
||||
.active-chat-user-profile-sidebar,
|
||||
.user-profile-sidebar {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-header,
|
||||
.active-chat-header {
|
||||
@extend %chat-header;
|
||||
}
|
||||
|
||||
.chat-list-search {
|
||||
.v-field__outline__start {
|
||||
flex-basis: 20px !important;
|
||||
border-radius: 28px 0 0 28px !important;
|
||||
}
|
||||
|
||||
.v-field__outline__end {
|
||||
border-radius: 0 28px 28px 0 !important;
|
||||
}
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
.v-field__outline__start {
|
||||
flex-basis: 20px !important;
|
||||
border-radius: 0 28px 28px 0 !important;
|
||||
}
|
||||
|
||||
.v-field__outline__end {
|
||||
border-radius: 28px 0 0 28px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-sidebar {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content-container {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
background-color: v-bind(chatContentContainerBg);
|
||||
|
||||
// Adjust the padding so text field height stays 48px
|
||||
.chat-message-input {
|
||||
.v-field__append-inner {
|
||||
align-items: center;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.v-field--appended {
|
||||
padding-inline-end: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-user-profile-badge {
|
||||
.v-badge__badge {
|
||||
/* stylelint-disable liberty/use-logical-spec */
|
||||
min-width: 12px !important;
|
||||
height: 0.75rem;
|
||||
/* stylelint-enable liberty/use-logical-spec */
|
||||
}
|
||||
}
|
||||
</style>
|
107
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal file
107
resources/js/pages/apps/ecommerce/customer/details/[id].vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import CustomerBioPanel from '@/views/apps/ecommerce/customer/view/CustomerBioPanel.vue'
|
||||
import CustomerTabAddressAndBilling from '@/views/apps/ecommerce/customer/view/CustomerTabAddressAndBilling.vue'
|
||||
import CustomerTabNotification from '@/views/apps/ecommerce/customer/view/CustomerTabNotification.vue'
|
||||
import CustomerTabOverview from '@/views/apps/ecommerce/customer/view/CustomerTabOverview.vue'
|
||||
import CustomerTabSecurity from '@/views/apps/ecommerce/customer/view/CustomerTabSecurity.vue'
|
||||
|
||||
const route = useRoute('apps-ecommerce-customer-details-id')
|
||||
const customerData = ref()
|
||||
const userTab = ref(null)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
icon: 'ri-group-line',
|
||||
title: 'Overview',
|
||||
},
|
||||
{
|
||||
icon: 'ri-lock-line',
|
||||
title: 'Security',
|
||||
},
|
||||
{
|
||||
icon: 'ri-map-pin-line',
|
||||
title: 'Address & Billing',
|
||||
},
|
||||
{
|
||||
icon: 'ri-notification-3-line',
|
||||
title: 'Notifications',
|
||||
},
|
||||
]
|
||||
|
||||
const { data, error } = await useApi(`/apps/ecommerce/customers/${ route.params.id }`)
|
||||
if (error.value)
|
||||
console.log(error.value)
|
||||
else if (data.value)
|
||||
customerData.value = data.value
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 👉 Header -->
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-y-4 mb-6">
|
||||
<div>
|
||||
<h4 class="text-h4">
|
||||
Customer ID #{{ route.params.id }}
|
||||
</h4>
|
||||
<p class="text-body-1 mb-0">
|
||||
Aug 17, 2020, 5:48 (ET)
|
||||
</p>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete Customer
|
||||
</VBtn>
|
||||
</div>
|
||||
<!-- 👉 Customer Profile -->
|
||||
<VRow v-if="customerData">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="5"
|
||||
lg="4"
|
||||
>
|
||||
<CustomerBioPanel :customer-data="customerData" />
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="7"
|
||||
lg="8"
|
||||
>
|
||||
<VTabs
|
||||
v-model="userTab"
|
||||
class="v-tabs-pill mb-6 6 disable-tab-transition"
|
||||
>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.icon"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
:icon="tab.icon"
|
||||
/>
|
||||
<span>{{ tab.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="userTab"
|
||||
class="mb-6 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem>
|
||||
<CustomerTabOverview />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabSecurity />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabAddressAndBilling />
|
||||
</VWindowItem>
|
||||
<VWindowItem>
|
||||
<CustomerTabNotification />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
187
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal file
187
resources/js/pages/apps/ecommerce/customer/list/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import ECommerceAddCustomerDrawer from '@/views/apps/ecommerce/ECommerceAddCustomerDrawer.vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const isAddCustomerDrawerOpen = ref(false)
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
// Data table Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'Customer',
|
||||
key: 'customer',
|
||||
},
|
||||
{
|
||||
title: 'Customer Id',
|
||||
key: 'customerId',
|
||||
},
|
||||
{
|
||||
title: 'Country',
|
||||
key: 'country',
|
||||
},
|
||||
{
|
||||
title: 'Orders',
|
||||
key: 'orders',
|
||||
},
|
||||
{
|
||||
title: 'Total Spent',
|
||||
key: 'totalSpent',
|
||||
},
|
||||
]
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const { data: customerData } = await useApi(createUrl('/apps/ecommerce/customers', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const customers = computed(() => customerData.value.customers)
|
||||
const totalCustomers = computed(() => customerData.value.total)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between flex-wrap gap-y-4">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
style="max-inline-size: 200px; min-inline-size: 200px;"
|
||||
density="compact"
|
||||
placeholder="Search .."
|
||||
/>
|
||||
<div class="d-flex flex-row gap-4 align-center flex-wrap">
|
||||
<VBtn
|
||||
prepend-icon="ri-upload-2-line"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
<VBtn
|
||||
prepend-icon="ri-add-line"
|
||||
@click="isAddCustomerDrawerOpen = !isAddCustomerDrawerOpen"
|
||||
>
|
||||
Add Customer
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:items="customers"
|
||||
item-value="customer"
|
||||
:headers="headers"
|
||||
:items-length="totalCustomers"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.customer="{ item }">
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:image="item.avatar"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: item.customerId } }"
|
||||
class="text-h6 font-weight-medium"
|
||||
>
|
||||
{{ item.customer }}
|
||||
</RouterLink>
|
||||
<span class="text-sm">{{ item.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.customerId="{ item }">
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
#{{ item.customerId }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<template #item.orders="{ item }">
|
||||
{{ item.order }}
|
||||
</template>
|
||||
|
||||
<template #item.country="{ item }">
|
||||
<div class="d-flex gap-x-2">
|
||||
<img
|
||||
:src="item.countryFlag"
|
||||
height="22"
|
||||
width="22"
|
||||
>
|
||||
<span class="text-body-1">{{ item.country }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.totalSpent="{ item }">
|
||||
<h6 class="text-h6">
|
||||
${{ item.totalSpent }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalCustomers) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalCustomers / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalCustomers / itemsPerPage) ? page = Math.ceil(totalCustomers / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
<ECommerceAddCustomerDrawer v-model:is-drawer-open="isAddCustomerDrawerOpen" />
|
||||
</div>
|
||||
</template>
|
587
resources/js/pages/apps/ecommerce/manage-review.vue
Normal file
587
resources/js/pages/apps/ecommerce/manage-review.vue
Normal file
@@ -0,0 +1,587 @@
|
||||
<script setup>
|
||||
const selectedStatus = ref('All')
|
||||
const searchQuery = ref('')
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const {
|
||||
data: ReviewData,
|
||||
execute: fetchReviews,
|
||||
} = await useApi(createUrl('/apps/ecommerce/reviews', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const reviews = computed(() => ReviewData.value.reviews)
|
||||
const totalReviews = computed(() => ReviewData.value.total)
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const deleteReview = async id => {
|
||||
await $api(`/apps/ecommerce/reviews/${ id }`, { method: 'DELETE' })
|
||||
fetchReviews()
|
||||
}
|
||||
|
||||
const reviewCardData = [
|
||||
{
|
||||
rating: 5,
|
||||
value: 124,
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
value: 40,
|
||||
},
|
||||
{
|
||||
rating: 3,
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
rating: 2,
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
rating: 1,
|
||||
value: 2,
|
||||
},
|
||||
]
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Product',
|
||||
key: 'product',
|
||||
},
|
||||
{
|
||||
title: 'Reviewer',
|
||||
key: 'reviewer',
|
||||
},
|
||||
{
|
||||
title: 'Review',
|
||||
key: 'review',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Date',
|
||||
key: 'date',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const labelColor = 'rgba(var(--v-theme-on-surface), var(--v-disabled-opacity))'
|
||||
|
||||
const reviewStatChartSeries = [{
|
||||
data: [
|
||||
20,
|
||||
40,
|
||||
60,
|
||||
80,
|
||||
100,
|
||||
80,
|
||||
60,
|
||||
],
|
||||
}]
|
||||
|
||||
const reviewStatChartConfig = {
|
||||
chart: {
|
||||
height: 160,
|
||||
width: 190,
|
||||
type: 'bar',
|
||||
toolbar: { show: false },
|
||||
},
|
||||
legend: { show: false },
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
top: -25,
|
||||
bottom: -12,
|
||||
},
|
||||
},
|
||||
colors: [
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
'rgba(var(--v-theme-success), 1)',
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
'rgba(var(--v-theme-success), var(--v-activated-opacity))',
|
||||
],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
barHeight: '75%',
|
||||
columnWidth: '35%',
|
||||
borderRadius: 5,
|
||||
distributed: true,
|
||||
},
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
xaxis: {
|
||||
categories: [
|
||||
'M',
|
||||
'T',
|
||||
'W',
|
||||
'T',
|
||||
'F',
|
||||
'S',
|
||||
'S',
|
||||
],
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { show: false },
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontSize: '13px',
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: { labels: { show: false } },
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 0,
|
||||
options: {
|
||||
chart: { width: '100%' },
|
||||
plotOptions: { bar: { columnWidth: '40%' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1440,
|
||||
options: {
|
||||
chart: {
|
||||
height: 150,
|
||||
width: 190,
|
||||
toolbar: { show: !1 },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1400,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1200,
|
||||
options: {
|
||||
chart: {
|
||||
height: 130,
|
||||
width: 190,
|
||||
toolbar: { show: !1 },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 992,
|
||||
chart: {
|
||||
height: 150,
|
||||
width: 190,
|
||||
toolbar: { show: !1 },
|
||||
},
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 5,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 883,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 5,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
chart: {
|
||||
height: 150,
|
||||
width: 190,
|
||||
toolbar: { show: !1 },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 4,
|
||||
columnWidth: '40%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
chart: {
|
||||
width: '100%',
|
||||
height: '200',
|
||||
type: 'bar',
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
columnWidth: '30% ',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 420,
|
||||
options: {
|
||||
plotOptions: {
|
||||
chart: {
|
||||
width: '100%',
|
||||
height: '200',
|
||||
type: 'bar',
|
||||
},
|
||||
bar: {
|
||||
borderRadius: 3,
|
||||
columnWidth: '30%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<!-- 👉 Total Review Card -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<div :class="$vuetify.display.smAndUp ? 'border-e' : 'border-b'">
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<h4 class="text-h3 text-primary">
|
||||
4.89
|
||||
</h4>
|
||||
<VIcon
|
||||
icon="ri-star-smile-line"
|
||||
color="primary"
|
||||
size="32"
|
||||
/>
|
||||
</div>
|
||||
<h6 class="my-2 text-h6">
|
||||
Total 187 reviews
|
||||
</h6>
|
||||
<div class="mb-2">
|
||||
All reviews are from genuine customers
|
||||
</div>
|
||||
<VChip
|
||||
color="primary"
|
||||
size="small"
|
||||
:class="$vuetify.display.smAndUp ? '' : 'mb-4'"
|
||||
>
|
||||
+5 This week
|
||||
</VChip>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in reviewCardData"
|
||||
:key="index"
|
||||
class="d-flex align-center gap-4 mb-3"
|
||||
>
|
||||
<div class="text-sm text-no-wrap">
|
||||
{{ item.rating }} Star
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<VProgressLinear
|
||||
color="primary"
|
||||
height="8"
|
||||
:model-value="(item.value / 185) * 100"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="5"
|
||||
>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-2">
|
||||
Reviews statistics
|
||||
</h5>
|
||||
<div class="mb-9">
|
||||
<span class="me-2">12 New Reviews</span>
|
||||
<VChip
|
||||
color="success"
|
||||
size="small"
|
||||
>
|
||||
+8.4%
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-high-emphasis text-body-1 mb-2">
|
||||
<span class="text-success">87%</span> Positive Reviews
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
Weekly Report
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="7"
|
||||
>
|
||||
<div class="d-flex justify-start justify-sm-end">
|
||||
<VueApexCharts
|
||||
id="shipment-statistics"
|
||||
type="bar"
|
||||
height="150"
|
||||
:options="reviewStatChartConfig"
|
||||
:series="reviewStatChartSeries"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between flex-wrap gap-y-4">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
style="max-inline-size: 250px; min-inline-size: 200px;"
|
||||
placeholder="Search"
|
||||
density="compact"
|
||||
/>
|
||||
<div class="d-flex flex-row gap-4 align-center flex-wrap">
|
||||
<VSelect
|
||||
v-model="selectedStatus"
|
||||
style="min-inline-size: 6.25rem;"
|
||||
density="compact"
|
||||
:items="[
|
||||
{ title: 'All', value: 'All' },
|
||||
{ title: 'Published', value: 'Published' },
|
||||
{ title: 'Pending', value: 'Pending' },
|
||||
]"
|
||||
/>
|
||||
<VBtn prepend-icon="ri-upload-2-line">
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
:items="reviews"
|
||||
show-select
|
||||
:items-length="totalReviews"
|
||||
item-value="id"
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.product="{ item }">
|
||||
<div class="d-flex gap-x-4 align-center">
|
||||
<VAvatar
|
||||
:image="item.productImage"
|
||||
:size="38"
|
||||
variant="tonal"
|
||||
rounded
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<h6 class="text-h6">
|
||||
{{ item.product }}
|
||||
</h6>
|
||||
<span class="text-sm text-wrap clamp-text">{{ item.companyName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.reviewer="{ item }">
|
||||
<div class="d-flex align-center gap-x-4">
|
||||
<VAvatar
|
||||
:image="item.avatar"
|
||||
size="34"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: 478426 } }"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
{{ item.reviewer }}
|
||||
</RouterLink>
|
||||
<span class="text-body-2">{{ item.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.review="{ item }">
|
||||
<div class="py-4">
|
||||
<VRating
|
||||
:size="24"
|
||||
readonly
|
||||
:model-value="item.review"
|
||||
/>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ item.head }}
|
||||
</h6>
|
||||
<p class="text-sm text-medium-emphasis text-wrap mb-0">
|
||||
{{ item.para }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.date="{ item }">
|
||||
<span class="text-body-1">{{ new Date(item.date).toDateString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="item.status === 'Published' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="view"
|
||||
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.id } }"
|
||||
>
|
||||
View
|
||||
</VListItem>
|
||||
<VListItem
|
||||
value="delete"
|
||||
@click="deleteReview(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalReviews) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalReviews / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalReviews / itemsPerPage) ? page = Math.ceil(totalReviews / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
</style>
|
1003
resources/js/pages/apps/ecommerce/order/AddOrder.vue
Normal file
1003
resources/js/pages/apps/ecommerce/order/AddOrder.vue
Normal file
File diff suppressed because it is too large
Load Diff
413
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal file
413
resources/js/pages/apps/ecommerce/order/details/[id].vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<script setup>
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import product21 from '@images/ecommerce-images/product-21.png'
|
||||
import product22 from '@images/ecommerce-images/product-22.png'
|
||||
import product23 from '@images/ecommerce-images/product-23.png'
|
||||
import product24 from '@images/ecommerce-images/product-24.png'
|
||||
|
||||
const route = useRoute('apps-ecommerce-order-details-id')
|
||||
const isConfirmDialogVisible = ref(false)
|
||||
const isUserInfoEditDialogVisible = ref(false)
|
||||
const isEditAddressDialogVisible = ref(false)
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Product',
|
||||
key: 'productName',
|
||||
},
|
||||
{
|
||||
title: 'Price',
|
||||
key: 'price',
|
||||
},
|
||||
{
|
||||
title: 'Quantity',
|
||||
key: 'quantity',
|
||||
},
|
||||
{
|
||||
title: 'Total',
|
||||
key: 'total',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const orderData = [
|
||||
{
|
||||
productName: 'OnePlus 7 Pro',
|
||||
productImage: product21,
|
||||
brand: 'OnePlus',
|
||||
price: 799,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
productName: 'Magic Mouse',
|
||||
productImage: product22,
|
||||
brand: 'Apple',
|
||||
price: 89,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
productName: 'Wooden Chair',
|
||||
productImage: product23,
|
||||
brand: 'insofer',
|
||||
price: 289,
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
productName: 'Air Jorden',
|
||||
productImage: product24,
|
||||
brand: 'Nike',
|
||||
price: 299,
|
||||
quantity: 2,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-y-4 mb-6">
|
||||
<div>
|
||||
<div class="d-flex gap-2 align-center mb-2 flex-wrap">
|
||||
<h5 class="text-h5">
|
||||
Order #{{ route.params.id }}
|
||||
</h5>
|
||||
<div class="d-flex gap-x-2">
|
||||
<VChip
|
||||
variant="tonal"
|
||||
color="success"
|
||||
size="small"
|
||||
>
|
||||
Paid
|
||||
</VChip>
|
||||
<VChip
|
||||
variant="tonal"
|
||||
color="info"
|
||||
size="small"
|
||||
>
|
||||
Ready to Pickup
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-body-1">
|
||||
Aug 17, 2020, 5:48 (ET)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
@click="isConfirmDialogVisible = !isConfirmDialogVisible"
|
||||
>
|
||||
Delete Order
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<!-- 👉 Order Details -->
|
||||
<VCard class="mb-6">
|
||||
<VCardItem>
|
||||
<template #title>
|
||||
<h5 class="text-h5">
|
||||
Order Details
|
||||
</h5>
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="text-primary cursor-pointer">Edit</span>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDataTable
|
||||
:headers="headers"
|
||||
:items="orderData"
|
||||
item-value="productName"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
>
|
||||
<template #item.productName="{ item }">
|
||||
<div class="d-flex gap-x-3">
|
||||
<VAvatar
|
||||
size="34"
|
||||
variant="tonal"
|
||||
:image="item.productImage"
|
||||
rounded
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-column align-center">
|
||||
<h6 class="text-h6">
|
||||
{{ item.productName }}
|
||||
</h6>
|
||||
|
||||
<span class="text-sm text-start align-self-start">
|
||||
{{ item.brand }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.price="{ item }">
|
||||
<span>${{ item.price }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.total="{ item }">
|
||||
<span>
|
||||
${{ item.price * item.quantity }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<div class="d-flex align-end flex-column">
|
||||
<table class="text-high-emphasis">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="200px">
|
||||
Subtotal:
|
||||
</td>
|
||||
<td class="font-weight-medium">
|
||||
$2,093
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shipping fee: </td>
|
||||
<td class="font-weight-medium">
|
||||
$2
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tax: </td>
|
||||
<td class="font-weight-medium">
|
||||
$28
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-medium">
|
||||
Total:
|
||||
</td>
|
||||
<td class="font-weight-medium">
|
||||
$2,113
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Shipping Activity -->
|
||||
<VCard title="Shipping Activity">
|
||||
<VCardText>
|
||||
<VTimeline
|
||||
truncate-line="both"
|
||||
align="start"
|
||||
side="end"
|
||||
line-inset="10"
|
||||
line-color="primary"
|
||||
density="compact"
|
||||
class="v-timeline-density-compact"
|
||||
>
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Order was placed (Order ID: #32543)</span>
|
||||
<span class="app-timeline-meta">Tuesday 11:29 AM</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Your order has been placed successfully
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Pick-up</span>
|
||||
<span class="app-timeline-meta">Wednesday 11:29 AM</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Pick-up scheduled with courier
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Dispatched</span>
|
||||
<span class="app-timeline-meta">Thursday 8:15 AM</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Item has been picked up by courier.
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Package arrived</span>
|
||||
<span class="app-timeline-meta">Saturday 15:20 AM</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Package arrived at an Amazon facility, NY
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Dispatched for delivery</span>
|
||||
<span class="app-timeline-meta">Today 14:12 PM</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Package has left an Amazon facility , NY
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
<VTimelineItem
|
||||
dot-color="primary"
|
||||
size="x-small"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">Delivery</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
Package will be delivered by tomorrow
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<!-- 👉 Customer Details -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="d-flex flex-column gap-y-6">
|
||||
<h5 class="text-h5">
|
||||
Customer Details
|
||||
</h5>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
:image="avatar1"
|
||||
class="me-3"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
Shamus Tuttle
|
||||
</div>
|
||||
<span>Customer ID: #47389</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="success"
|
||||
class="me-3"
|
||||
>
|
||||
<VIcon icon="ri-shopping-cart-line" />
|
||||
</VAvatar>
|
||||
|
||||
<h6 class="text-h6">
|
||||
12 Orders
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<div class="d-flex justify-space-between gap-1 text-body-2">
|
||||
<h6 class="text-h6">
|
||||
Contact Info
|
||||
</h6>
|
||||
<span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="isUserInfoEditDialogVisible = !isUserInfoEditDialogVisible"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
<span>Email: Sheldon88@yahoo.com</span>
|
||||
<span>Mobile: +1 (609) 972-22-22</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Shipping Address -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-6">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
Shipping Address
|
||||
</div>
|
||||
<span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
|
||||
>Edit</span>
|
||||
</div>
|
||||
<div>
|
||||
45 Rocker Terrace <br> Latheronwheel <br> KW5 8NW, London <br> UK
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Billing Address -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-3">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
Billing Address
|
||||
</div>
|
||||
<span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="isEditAddressDialogVisible = !isEditAddressDialogVisible"
|
||||
>Edit</span>
|
||||
</div>
|
||||
<div>
|
||||
45 Rocker Terrace <br> Latheronwheel <br> KW5 8NW, London <br> UK
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h6 class="text-h6 mb-1">
|
||||
Mastercard
|
||||
</h6>
|
||||
<div class="text-base">
|
||||
Card Number: ******4291
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:isDialogVisible="isConfirmDialogVisible"
|
||||
confirmation-question="Are you sure to cancel your Order?"
|
||||
cancel-msg="Order cancelled!!"
|
||||
cancel-title="Cancelled"
|
||||
confirm-msg="Your order cancelled successfully."
|
||||
confirm-title="Cancelled!"
|
||||
/>
|
||||
|
||||
<UserInfoEditDialog v-model:isDialogVisible="isUserInfoEditDialogVisible" />
|
||||
|
||||
<AddEditAddressDialog v-model:isDialogVisible="isEditAddressDialogVisible" />
|
||||
</div>
|
||||
</template>
|
549
resources/js/pages/apps/ecommerce/order/list/AddPrescrption.vue
Normal file
549
resources/js/pages/apps/ecommerce/order/list/AddPrescrption.vue
Normal file
@@ -0,0 +1,549 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
import { VForm } from 'vuetify/components/VForm';
|
||||
import { useStore } from 'vuex';
|
||||
const route = useRoute();
|
||||
const isMobile = ref(window.innerWidth <= 768);
|
||||
const store = useStore()
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
const states = ref([
|
||||
{ name: 'Alabama', abbreviation: 'AL' },
|
||||
{ name: 'Alaska', abbreviation: 'AK' },
|
||||
{ name: 'Arizona', abbreviation: 'AZ' },
|
||||
{ name: 'Arkansas', abbreviation: 'AR' },
|
||||
{ name: 'Howland Island', abbreviation: 'UM-84' },
|
||||
{ name: 'Delaware', abbreviation: 'DE' },
|
||||
{ name: 'Maryland', abbreviation: 'MD' },
|
||||
{ name: 'Baker Island', abbreviation: 'UM-81' },
|
||||
{ name: 'Kingman Reef', abbreviation: 'UM-89' },
|
||||
{ name: 'New Hampshire', abbreviation: 'NH' },
|
||||
{ name: 'Wake Island', abbreviation: 'UM-79' },
|
||||
{ name: 'Kansas', abbreviation: 'KS' },
|
||||
{ name: 'Texas', abbreviation: 'TX' },
|
||||
{ name: 'Nebraska', abbreviation: 'NE' },
|
||||
{ name: 'Vermont', abbreviation: 'VT' },
|
||||
{ name: 'Jarvis Island', abbreviation: 'UM-86' },
|
||||
{ name: 'Hawaii', abbreviation: 'HI' },
|
||||
{ name: 'Guam', abbreviation: 'GU' },
|
||||
{ name: 'United States Virgin Islands', abbreviation: 'VI' },
|
||||
{ name: 'Utah', abbreviation: 'UT' },
|
||||
{ name: 'Oregon', abbreviation: 'OR' },
|
||||
{ name: 'California', abbreviation: 'CA' },
|
||||
{ name: 'New Jersey', abbreviation: 'NJ' },
|
||||
{ name: 'North Dakota', abbreviation: 'ND' },
|
||||
{ name: 'Kentucky', abbreviation: 'KY' },
|
||||
{ name: 'Minnesota', abbreviation: 'MN' },
|
||||
{ name: 'Oklahoma', abbreviation: 'OK' },
|
||||
{ name: 'Pennsylvania', abbreviation: 'PA' },
|
||||
{ name: 'New Mexico', abbreviation: 'NM' },
|
||||
{ name: 'American Samoa', abbreviation: 'AS' },
|
||||
{ name: 'Illinois', abbreviation: 'IL' },
|
||||
{ name: 'Michigan', abbreviation: 'MI' },
|
||||
{ name: 'Virginia', abbreviation: 'VA' },
|
||||
{ name: 'Johnston Atoll', abbreviation: 'UM-67' },
|
||||
{ name: 'West Virginia', abbreviation: 'WV' },
|
||||
{ name: 'Mississippi', abbreviation: 'MS' },
|
||||
{ name: 'Northern Mariana Islands', abbreviation: 'MP' },
|
||||
{ name: 'United States Minor Outlying Islands', abbreviation: 'UM' },
|
||||
{ name: 'Massachusetts', abbreviation: 'MA' },
|
||||
{ name: 'Connecticut', abbreviation: 'CT' },
|
||||
{ name: 'Florida', abbreviation: 'FL' },
|
||||
{ name: 'District of Columbia', abbreviation: 'DC' },
|
||||
{ name: 'Midway Atoll', abbreviation: 'UM-71' },
|
||||
{ name: 'Navassa Island', abbreviation: 'UM-76' },
|
||||
{ name: 'Indiana', abbreviation: 'IN' },
|
||||
{ name: 'Wisconsin', abbreviation: 'WI' },
|
||||
{ name: 'Wyoming', abbreviation: 'WY' },
|
||||
{ name: 'South Carolina', abbreviation: 'SC' },
|
||||
{ name: 'South Dakota', abbreviation: 'SD' },
|
||||
{ name: 'Montana', abbreviation: 'MT' },
|
||||
{ name: 'North Carolina', abbreviation: 'NC' },
|
||||
{ name: 'Palmyra Atoll', abbreviation: 'UM-95' },
|
||||
{ name: 'Puerto Rico', abbreviation: 'PR' },
|
||||
{ name: 'Colorado', abbreviation: 'CO' },
|
||||
{ name: 'Missouri', abbreviation: 'MO' },
|
||||
{ name: 'New York', abbreviation: 'NY' },
|
||||
{ name: 'Maine', abbreviation: 'ME' },
|
||||
{ name: 'Tennessee', abbreviation: 'TN' },
|
||||
{ name: 'Georgia', abbreviation: 'GA' },
|
||||
{ name: 'Louisiana', abbreviation: 'LA' },
|
||||
{ name: 'Nevada', abbreviation: 'NV' },
|
||||
{ name: 'Iowa', abbreviation: 'IA' },
|
||||
{ name: 'Idaho', abbreviation: 'ID' },
|
||||
{ name: 'Rhode Island', abbreviation: 'RI' },
|
||||
{ name: 'Washington', abbreviation: 'WA' },
|
||||
{ name: 'Ohio', abbreviation: 'OH' },
|
||||
// ... (add the rest of the states)
|
||||
]);
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
const prescription_id = ref([]);
|
||||
const sortedStates = computed(() => {
|
||||
return states.value.slice().sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
const valid = ref(false);
|
||||
const form = ref(null);
|
||||
const getFieldRules = (fieldName, errorMessage) => {
|
||||
if (fieldName) {
|
||||
return [
|
||||
(v) => !!v || `${errorMessage}`,
|
||||
// Add more validation rules as needed
|
||||
];
|
||||
}
|
||||
};
|
||||
const headers = [
|
||||
|
||||
{ key: 'name', title: 'Name' },
|
||||
{ key: 'brand', title: 'Brand' },
|
||||
{ key: 'from', title: 'From' },
|
||||
{ key: 'direction_quantity', title: 'Direction Quantity' },
|
||||
{ key: 'dosage', title: 'Dosage' },
|
||||
{ key: 'quantity', title: 'Quantity' },
|
||||
{ key: 'refill_quantity', title: 'Refill Quantity' },
|
||||
{ key: 'actions', title: 'Action' },
|
||||
];
|
||||
const openDialog = (user, type) => {
|
||||
console.log("userId", type);
|
||||
|
||||
|
||||
if (type == "Prescription") {
|
||||
console.log("enter ne value");
|
||||
prescriptionModel.value = true;
|
||||
}
|
||||
if (type == "Prescription Form") {
|
||||
console.log("enter ne value");
|
||||
prescriptionModelForm.value = true;
|
||||
}
|
||||
|
||||
};
|
||||
const prescriptionForm = async () => {
|
||||
console.log("toggelUserValue.value.", prescription_id.value);
|
||||
// isLoadingVisible.value = true
|
||||
if (form.value.validate()) {
|
||||
console.log('item [[[]]]',medicines.value,prescription_id.value)
|
||||
|
||||
await store.dispatch("savePercriptionOrderDetail", {
|
||||
medicines: medicines.value,
|
||||
prescription_id: prescription_id.value,
|
||||
patient_id:props.patientId,
|
||||
order_id:route.params.id,
|
||||
brand: brand.value,
|
||||
from: from.value,
|
||||
dosage: dosage.value,
|
||||
quantity: quantity.value,
|
||||
direction_quantity: direction_quantity.value,
|
||||
direction_one: direction_one.value,
|
||||
direction_two: direction_two.value,
|
||||
refill_quantity: refil_quantity.value,
|
||||
dont_substitute: dont_substitute.value,
|
||||
comments: comments.value,
|
||||
});
|
||||
if (!store.getters.getErrorMsg) {
|
||||
emit('patientAdded', 'success')
|
||||
prescriptionModel.value = false;
|
||||
medicines.value = null;
|
||||
prescription_id.value = null;
|
||||
|
||||
brand.value = null;
|
||||
from.value = null;
|
||||
dosage.value = null;
|
||||
quantity.value = null;
|
||||
direction_quantity.value = null;
|
||||
direction_one.value = null;
|
||||
direction_two.value = null;
|
||||
refil_quantity.value = null;
|
||||
dont_substitute.value = null;
|
||||
comments.value = null;
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen','patientAdded'])
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
const genders = ref([
|
||||
{ name: 'Male', abbreviation: 'Male' },
|
||||
{ name: 'Female', abbreviation: 'Female' },
|
||||
{ name: 'Other', abbreviation: 'Other' },
|
||||
]);
|
||||
const prescriptionModel = ref(false);
|
||||
const prescriptionModelForm = ref(false);
|
||||
const selectedMedicines = ref([]);
|
||||
const toggelUserValue = ref(null)
|
||||
const medicines = ref("");
|
||||
const brand = ref("");
|
||||
const from = ref("");
|
||||
const dosage = ref("");
|
||||
const quantity = ref("");
|
||||
const direction_quantity = ref("");
|
||||
const direction_one = ref("");
|
||||
const direction_two = ref("");
|
||||
const refil_quantity = ref("");
|
||||
const dont_substitute = ref("");
|
||||
const comments = ref("");
|
||||
const search = ref("");
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const itemsPerPage = ref(10);
|
||||
const pageCount = ref(0);
|
||||
const itemsPrescriptions = ref([]);
|
||||
|
||||
const isBillingAddress = ref(false)
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
const { valid } = await refVForm.value.validate()
|
||||
if (valid) {
|
||||
if (calculateAge(dob.value) >= 18) {
|
||||
await store.dispatch('savePercriptionOrder', {
|
||||
first_name: first_name.value,
|
||||
last_name: last_name.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
phone_no: phone_no.value,
|
||||
dob: dob.value,
|
||||
address: addressLine1.value,
|
||||
city: city.value,
|
||||
state: state.value,
|
||||
zip_code:zip_code.value,
|
||||
country: country.value,
|
||||
gender: gender.value
|
||||
})
|
||||
} else {
|
||||
store.dispatch('updateErrorMessage', 'Patient must be 18+')
|
||||
}
|
||||
if (!store.getters.getErrorMsg) {
|
||||
emit('patientAdded', 'success')
|
||||
first_name.value = null
|
||||
last_name.value = null
|
||||
email.value = null
|
||||
password.value = null
|
||||
phone_no.value = null
|
||||
dob.value = null
|
||||
addressLine1.value = null
|
||||
city.value = null
|
||||
state.value = null
|
||||
country.value = null
|
||||
zip_code.value=null
|
||||
}
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
}
|
||||
const selectedItem = async (item) => {
|
||||
|
||||
medicines.value = item.name
|
||||
brand.value = item.brand
|
||||
dosage.value = item.dosage
|
||||
dosage.value = item.dosage
|
||||
from.value = item.from
|
||||
quantity.value = item.quantity
|
||||
direction_quantity.value = item.direction_quantity
|
||||
refil_quantity.value = item.refill_quantity
|
||||
prescription_id.value = item.id
|
||||
prescriptionModelForm.value = false
|
||||
|
||||
}
|
||||
onMounted(async () => {
|
||||
window.addEventListener("resize", checkMobile);
|
||||
await store.dispatch('orderPrecriptionList')
|
||||
itemsPrescriptions.value = store.getters.getOrderPrecriptionList
|
||||
console.log(itemsPrescriptions.value)
|
||||
loading.value=false
|
||||
});
|
||||
const resetForm = () => {
|
||||
refVForm.value?.reset()
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
:model-value="props.isDrawerOpen"
|
||||
temporary
|
||||
location="end"
|
||||
width="800"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Add Prescription"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
<VDivider />
|
||||
|
||||
<VCard flat>
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="h-100"
|
||||
>
|
||||
<VCardText style="block-size: calc(100vh - 5rem);">
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
|
||||
|
||||
|
||||
<v-form ref="form" v-model="valid" class="mt-6">
|
||||
<v-row>
|
||||
<v-col cols="12" md="2" v-if="isMobile">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="btn"
|
||||
style="height: 54px"
|
||||
@click.stop="
|
||||
openDialog(toggelUserValue, 'Prescription Form')
|
||||
"
|
||||
>
|
||||
Prescription
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="10">
|
||||
<v-text-field
|
||||
label="Medicine"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'Medicine',
|
||||
'Medicine is required'
|
||||
)
|
||||
"
|
||||
v-model="medicines"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" v-if="!isMobile">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="btn"
|
||||
style="height: 54px"
|
||||
@click.stop="
|
||||
openDialog(toggelUserValue, 'Prescription Form')
|
||||
"
|
||||
>
|
||||
Prescription
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Brand"
|
||||
:rules="getFieldRules('brand', 'Brand is required')"
|
||||
v-model="brand"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="From"
|
||||
:rules="getFieldRules('from', 'From is required')"
|
||||
v-model="from"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Dosage"
|
||||
:rules="
|
||||
getFieldRules('dosage', 'Dosage is required')
|
||||
"
|
||||
v-model="dosage"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'quantity',
|
||||
'Quantity is required'
|
||||
)
|
||||
"
|
||||
v-model="quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'direction quantity',
|
||||
'Direction Quantity is required'
|
||||
)
|
||||
"
|
||||
v-model="direction_quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Refil Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'Refil Quantity',
|
||||
'Refil Quantity one is required'
|
||||
)
|
||||
"
|
||||
v-model="refil_quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction one"
|
||||
:rules="
|
||||
getFieldRules('', 'Direction one is required')
|
||||
"
|
||||
v-model="direction_one"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction Two"
|
||||
:rules="
|
||||
getFieldRules('', 'Direction Two is required')
|
||||
"
|
||||
v-model="direction_two"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row style="margin-bottom: 5px">
|
||||
<v-col cols="12" md="12">
|
||||
<!-- <v-text-field label="Comments" :rules="getFieldRules('', 'Comments is required')" v-model="comments"
|
||||
required></v-text-field> -->
|
||||
<v-textarea
|
||||
label="Comments"
|
||||
:rules="getFieldRules('', 'Comments is required')"
|
||||
v-model="comments"
|
||||
required
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
|
||||
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="me-4"
|
||||
@click="prescriptionForm"
|
||||
:disabled="!valid"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="resetForm"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
<v-dialog v-model="prescriptionModelForm" max-width="1200">
|
||||
<v-card class="pa-3">
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-right cross">
|
||||
<v-btn
|
||||
icon
|
||||
color="transparent"
|
||||
small
|
||||
@click="prescriptionModelForm = false"
|
||||
>
|
||||
<v-icon>rdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="itemsPrescriptions"
|
||||
:search="search"
|
||||
:loading="loading"
|
||||
:page.sync="page"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
@page-count="pageCount = $event"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat :height="30">
|
||||
<v-toolbar-title>Prescriptions</v-toolbar-title>
|
||||
<v-divider
|
||||
class="mx-4"
|
||||
inset
|
||||
vertical
|
||||
></v-divider>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
small
|
||||
@click="selectedItem(item)"
|
||||
>Select</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
</style>
|
145
resources/js/pages/apps/ecommerce/order/list/EditOrderNote.vue
Normal file
145
resources/js/pages/apps/ecommerce/order/list/EditOrderNote.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
import { VForm } from 'vuetify/components/VForm';
|
||||
import { useStore } from 'vuex';
|
||||
const store = useStore()
|
||||
const router = useRouter();
|
||||
const route = useRoute()
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
userData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
const order_note = ref()
|
||||
const getSingleNote = computed(async () => {
|
||||
order_note.value = '';
|
||||
console.log("props",props.userData);
|
||||
if(props.userData.note)
|
||||
order_note.value = props.userData.note.note;
|
||||
// itemId.value=props.userData.id
|
||||
// editedItem.value.id= props.userData.id
|
||||
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen', 'addedMessage'])
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
const refVForm = ref()
|
||||
const onSubmit = async () => {
|
||||
const { valid } = await refVForm.value.validate()
|
||||
if (valid) {
|
||||
|
||||
await store.dispatch('UpdateNoteByID', {
|
||||
note: order_note.value,
|
||||
id: props.userData.note.id
|
||||
})
|
||||
|
||||
|
||||
if (!store.getters.getErrorMsg) {
|
||||
emit('addedMessage', 'success')
|
||||
order_note.value = null
|
||||
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
|
||||
emit('notes', store.getters.getPatientOrderDetail.appointment_notes)
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const resetForm = () => {
|
||||
refVForm.value?.reset()
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
// const roleData = ref([]);
|
||||
// const useSortedRole = () => {
|
||||
// const isLoading = ref(false);
|
||||
// const error = ref(null);
|
||||
|
||||
// const sortedRole = computed(() => {
|
||||
// const allOption = { id: '', role: 'Select Any' };
|
||||
// const sortedData = roleData.value.slice().sort((a, b) => {
|
||||
// return a.role.localeCompare(b.role);
|
||||
// });
|
||||
// return [allOption, ...sortedData];
|
||||
// });
|
||||
|
||||
// const fetchRoleData = async () => {
|
||||
// isLoading.value = true;
|
||||
// error.value = null;
|
||||
// try {
|
||||
// await store.dispatch('getAllRolesList');
|
||||
// roleData.value = store.getters.getRolesList || [];
|
||||
// console.log('Fetched Role data:', roleData.value);
|
||||
// } catch (e) {
|
||||
// console.error('Error fetching Role data:', e);
|
||||
// error.value = 'Failed to fetch Role data';
|
||||
// } finally {
|
||||
// isLoading.value = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// onBeforeMount(fetchRoleData);
|
||||
|
||||
// return { sortedRole, isLoading, error, fetchRoleData };
|
||||
// };
|
||||
// const { sortedRole, isLoading, error, fetchRoleData } = useSortedRole();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer :model-value="props.isDrawerOpen" temporary location="end" width="800"
|
||||
@update:model-value="handleDrawerModelValueUpdate">
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection title="Edit Note" @cancel="$emit('update:isDrawerOpen', false)" />
|
||||
<VDivider />
|
||||
|
||||
<VCard flat>
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }" class="h-100">
|
||||
<VCardText style="block-size: calc(100vh - 5rem);" v-if="getSingleNote">
|
||||
<VForm ref="refVForm" @submit.prevent="">
|
||||
<VRow>
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextarea v-model="order_note" label="Order Note" :rules="[requiredValidator]"
|
||||
placeholder="Note" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn type="submit" color="primary" class="me-4" @click="onSubmit">
|
||||
Update
|
||||
</VBtn>
|
||||
<VBtn color="error" variant="outlined" @click="resetForm">
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
</style>
|
534
resources/js/pages/apps/ecommerce/order/list/EditPrescrption.vue
Normal file
534
resources/js/pages/apps/ecommerce/order/list/EditPrescrption.vue
Normal file
@@ -0,0 +1,534 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
import { VForm } from 'vuetify/components/VForm';
|
||||
import { useStore } from 'vuex';
|
||||
const route = useRoute();
|
||||
const isMobile = ref(window.innerWidth <= 768);
|
||||
const store = useStore()
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
userData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
const states = ref([
|
||||
{ name: 'Alabama', abbreviation: 'AL' },
|
||||
{ name: 'Alaska', abbreviation: 'AK' },
|
||||
{ name: 'Arizona', abbreviation: 'AZ' },
|
||||
{ name: 'Arkansas', abbreviation: 'AR' },
|
||||
{ name: 'Howland Island', abbreviation: 'UM-84' },
|
||||
{ name: 'Delaware', abbreviation: 'DE' },
|
||||
{ name: 'Maryland', abbreviation: 'MD' },
|
||||
{ name: 'Baker Island', abbreviation: 'UM-81' },
|
||||
{ name: 'Kingman Reef', abbreviation: 'UM-89' },
|
||||
{ name: 'New Hampshire', abbreviation: 'NH' },
|
||||
{ name: 'Wake Island', abbreviation: 'UM-79' },
|
||||
{ name: 'Kansas', abbreviation: 'KS' },
|
||||
{ name: 'Texas', abbreviation: 'TX' },
|
||||
{ name: 'Nebraska', abbreviation: 'NE' },
|
||||
{ name: 'Vermont', abbreviation: 'VT' },
|
||||
{ name: 'Jarvis Island', abbreviation: 'UM-86' },
|
||||
{ name: 'Hawaii', abbreviation: 'HI' },
|
||||
{ name: 'Guam', abbreviation: 'GU' },
|
||||
{ name: 'United States Virgin Islands', abbreviation: 'VI' },
|
||||
{ name: 'Utah', abbreviation: 'UT' },
|
||||
{ name: 'Oregon', abbreviation: 'OR' },
|
||||
{ name: 'California', abbreviation: 'CA' },
|
||||
{ name: 'New Jersey', abbreviation: 'NJ' },
|
||||
{ name: 'North Dakota', abbreviation: 'ND' },
|
||||
{ name: 'Kentucky', abbreviation: 'KY' },
|
||||
{ name: 'Minnesota', abbreviation: 'MN' },
|
||||
{ name: 'Oklahoma', abbreviation: 'OK' },
|
||||
{ name: 'Pennsylvania', abbreviation: 'PA' },
|
||||
{ name: 'New Mexico', abbreviation: 'NM' },
|
||||
{ name: 'American Samoa', abbreviation: 'AS' },
|
||||
{ name: 'Illinois', abbreviation: 'IL' },
|
||||
{ name: 'Michigan', abbreviation: 'MI' },
|
||||
{ name: 'Virginia', abbreviation: 'VA' },
|
||||
{ name: 'Johnston Atoll', abbreviation: 'UM-67' },
|
||||
{ name: 'West Virginia', abbreviation: 'WV' },
|
||||
{ name: 'Mississippi', abbreviation: 'MS' },
|
||||
{ name: 'Northern Mariana Islands', abbreviation: 'MP' },
|
||||
{ name: 'United States Minor Outlying Islands', abbreviation: 'UM' },
|
||||
{ name: 'Massachusetts', abbreviation: 'MA' },
|
||||
{ name: 'Connecticut', abbreviation: 'CT' },
|
||||
{ name: 'Florida', abbreviation: 'FL' },
|
||||
{ name: 'District of Columbia', abbreviation: 'DC' },
|
||||
{ name: 'Midway Atoll', abbreviation: 'UM-71' },
|
||||
{ name: 'Navassa Island', abbreviation: 'UM-76' },
|
||||
{ name: 'Indiana', abbreviation: 'IN' },
|
||||
{ name: 'Wisconsin', abbreviation: 'WI' },
|
||||
{ name: 'Wyoming', abbreviation: 'WY' },
|
||||
{ name: 'South Carolina', abbreviation: 'SC' },
|
||||
{ name: 'South Dakota', abbreviation: 'SD' },
|
||||
{ name: 'Montana', abbreviation: 'MT' },
|
||||
{ name: 'North Carolina', abbreviation: 'NC' },
|
||||
{ name: 'Palmyra Atoll', abbreviation: 'UM-95' },
|
||||
{ name: 'Puerto Rico', abbreviation: 'PR' },
|
||||
{ name: 'Colorado', abbreviation: 'CO' },
|
||||
{ name: 'Missouri', abbreviation: 'MO' },
|
||||
{ name: 'New York', abbreviation: 'NY' },
|
||||
{ name: 'Maine', abbreviation: 'ME' },
|
||||
{ name: 'Tennessee', abbreviation: 'TN' },
|
||||
{ name: 'Georgia', abbreviation: 'GA' },
|
||||
{ name: 'Louisiana', abbreviation: 'LA' },
|
||||
{ name: 'Nevada', abbreviation: 'NV' },
|
||||
{ name: 'Iowa', abbreviation: 'IA' },
|
||||
{ name: 'Idaho', abbreviation: 'ID' },
|
||||
{ name: 'Rhode Island', abbreviation: 'RI' },
|
||||
{ name: 'Washington', abbreviation: 'WA' },
|
||||
{ name: 'Ohio', abbreviation: 'OH' },
|
||||
// ... (add the rest of the states)
|
||||
]);
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
const prescription_id = ref();
|
||||
const sortedStates = computed(() => {
|
||||
return states.value.slice().sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
const valid = ref(false);
|
||||
const form = ref(null);
|
||||
const getFieldRules = (fieldName, errorMessage) => {
|
||||
if (fieldName) {
|
||||
return [
|
||||
(v) => !!v || `${errorMessage}`,
|
||||
// Add more validation rules as needed
|
||||
];
|
||||
}
|
||||
};
|
||||
const headers = [
|
||||
|
||||
{ key: 'name', title: 'Name' },
|
||||
{ key: 'brand', title: 'Brand' },
|
||||
{ key: 'from', title: 'From' },
|
||||
{ key: 'direction_quantity', title: 'Direction Quantity' },
|
||||
{ key: 'dosage', title: 'Dosage' },
|
||||
{ key: 'quantity', title: 'Quantity' },
|
||||
{ key: 'refill_quantity', title: 'Refill Quantity' },
|
||||
{ key: 'actions', title: 'Action' },
|
||||
];
|
||||
const openDialog = (user, type) => {
|
||||
console.log("userId", user);
|
||||
|
||||
|
||||
if (type == "Prescription") {
|
||||
console.log("enter ne value");
|
||||
prescriptionModel.value = true;
|
||||
}
|
||||
if (type == "Prescription Form") {
|
||||
console.log("enter ne value");
|
||||
prescriptionModelForm.value = true;
|
||||
}
|
||||
|
||||
};
|
||||
const itemId = ref()
|
||||
const prescriptionForm = async () => {
|
||||
console.log("toggelUserValue.value.", prescription_id.value);
|
||||
// isLoadingVisible.value = true
|
||||
if (form.value.validate()) {
|
||||
|
||||
console.log(prescription_id.value,route.params.id)
|
||||
await store.dispatch("updatePercriptionOrderDetail", {
|
||||
id:itemId.value,
|
||||
medicines: medicines.value,
|
||||
prescription_id: prescription_id.value,
|
||||
patient_id:props.patientId,
|
||||
order_id:route.params.id,
|
||||
brand: brand.value,
|
||||
from: from.value,
|
||||
dosage: dosage.value,
|
||||
quantity: quantity.value,
|
||||
direction_quantity: direction_quantity.value,
|
||||
direction_one: direction_one.value,
|
||||
direction_two: direction_two.value,
|
||||
refill_quantity: refil_quantity.value,
|
||||
dont_substitute: dont_substitute.value,
|
||||
comments: comments.value,
|
||||
});
|
||||
if (!store.getters.getErrorMsg) {
|
||||
emit('patientAdded', 'success')
|
||||
prescriptionModel.value = false;
|
||||
medicines.value = null;
|
||||
prescription_id.value = null;
|
||||
|
||||
brand.value = null;
|
||||
from.value = null;
|
||||
dosage.value = null;
|
||||
quantity.value = null;
|
||||
direction_quantity.value = null;
|
||||
direction_one.value = null;
|
||||
direction_two.value = null;
|
||||
refil_quantity.value = null;
|
||||
dont_substitute.value = null;
|
||||
comments.value = null;
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen','patientAdded'])
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
const genders = ref([
|
||||
{ name: 'Male', abbreviation: 'Male' },
|
||||
{ name: 'Female', abbreviation: 'Female' },
|
||||
{ name: 'Other', abbreviation: 'Other' },
|
||||
]);
|
||||
const prescriptionModel = ref(false);
|
||||
const prescriptionModelForm = ref(false);
|
||||
const selectedMedicines = ref([]);
|
||||
const toggelUserValue = ref(null)
|
||||
const medicines = ref("");
|
||||
const brand = ref("");
|
||||
const from = ref("");
|
||||
const dosage = ref("");
|
||||
const quantity = ref("");
|
||||
const direction_quantity = ref("");
|
||||
const direction_one = ref("");
|
||||
const direction_two = ref("");
|
||||
const refil_quantity = ref("");
|
||||
const dont_substitute = ref("");
|
||||
const comments = ref("");
|
||||
const search = ref("");
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const itemsPerPage = ref(10);
|
||||
const pageCount = ref(0);
|
||||
const itemsPrescriptions = ref([]);
|
||||
|
||||
|
||||
|
||||
|
||||
const selectedItem = async (item) => {
|
||||
console.log(item)
|
||||
medicines.value = item.name
|
||||
brand.value = item.brand
|
||||
dosage.value = item.dosage
|
||||
dosage.value = item.dosage
|
||||
from.value = item.from
|
||||
quantity.value = item.quantity
|
||||
direction_quantity.value = item.direction_quantity
|
||||
refil_quantity.value = item.refill_quantity
|
||||
prescription_id.value = item.id
|
||||
prescriptionModelForm.value = false
|
||||
|
||||
}
|
||||
onMounted(async () => {
|
||||
window.addEventListener("resize", checkMobile);
|
||||
await store.dispatch('orderPrecriptionList')
|
||||
itemsPrescriptions.value = store.getters.getOrderPrecriptionList
|
||||
console.log(itemsPrescriptions.value)
|
||||
loading.value=false
|
||||
});
|
||||
const resetForm = () => {
|
||||
refVForm.value?.reset()
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
const getPrescrption = computed(async () => {
|
||||
if (props.userData) {
|
||||
itemId.value=props.userData.id
|
||||
medicines.value = props.userData.prescription.name
|
||||
prescription_id.value=props.userData.prescription_id
|
||||
brand.value=props.userData.brand
|
||||
from.value= props.userData.from
|
||||
dosage.value= props.userData.dosage
|
||||
quantity.value= props.userData.quantity
|
||||
direction_quantity.value = props.userData.direction_quantity
|
||||
direction_one.value= props.userData.direction_one,
|
||||
direction_two.value= props.userData.direction_two,
|
||||
comments.value= props.userData.comments,
|
||||
refil_quantity.value= props.userData.refill_quantity
|
||||
dont_substitute.value= props.userData.dont_substitute
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
:model-value="props.isDrawerOpen"
|
||||
temporary
|
||||
location="end"
|
||||
width="800"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Edit Prescription"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
<VDivider />
|
||||
|
||||
<VCard flat>
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="h-100"
|
||||
>
|
||||
<VCardText style="block-size: calc(100vh - 5rem);" v-if="getPrescrption">
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
|
||||
|
||||
|
||||
<v-form ref="form" v-model="valid" class="mt-6">
|
||||
<v-row>
|
||||
<v-col cols="12" md="2" v-if="isMobile">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="btn"
|
||||
style="height: 54px"
|
||||
@click.stop="
|
||||
openDialog(toggelUserValue, 'Prescription Form')
|
||||
"
|
||||
>
|
||||
Prescription
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="10">
|
||||
<v-text-field
|
||||
label="Medicine"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'Medicine',
|
||||
'Medicine is required'
|
||||
)
|
||||
"
|
||||
v-model="medicines"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" v-if="!isMobile">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="btn"
|
||||
style="height: 54px"
|
||||
@click.stop="
|
||||
openDialog(toggelUserValue, 'Prescription Form')
|
||||
"
|
||||
>
|
||||
Prescription
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Brand"
|
||||
:rules="getFieldRules('brand', 'Brand is required')"
|
||||
v-model="brand"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="From"
|
||||
:rules="getFieldRules('from', 'From is required')"
|
||||
v-model="from"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Dosage"
|
||||
:rules="
|
||||
getFieldRules('dosage', 'Dosage is required')
|
||||
"
|
||||
v-model="dosage"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'quantity',
|
||||
'Quantity is required'
|
||||
)
|
||||
"
|
||||
v-model="quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'direction quantity',
|
||||
'Direction Quantity is required'
|
||||
)
|
||||
"
|
||||
v-model="direction_quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Refil Quantity"
|
||||
:rules="
|
||||
getFieldRules(
|
||||
'Refil Quantity',
|
||||
'Refil Quantity one is required'
|
||||
)
|
||||
"
|
||||
v-model="refil_quantity"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction one"
|
||||
:rules="
|
||||
getFieldRules('', 'Direction one is required')
|
||||
"
|
||||
v-model="direction_one"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="Direction Two"
|
||||
:rules="
|
||||
getFieldRules('', 'Direction Two is required')
|
||||
"
|
||||
v-model="direction_two"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row style="margin-bottom: 5px">
|
||||
<v-col cols="12" md="12">
|
||||
<!-- <v-text-field label="Comments" :rules="getFieldRules('', 'Comments is required')" v-model="comments"
|
||||
required></v-text-field> -->
|
||||
<v-textarea
|
||||
label="Comments"
|
||||
:rules="getFieldRules('', 'Comments is required')"
|
||||
v-model="comments"
|
||||
required
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
|
||||
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="me-4"
|
||||
@click="prescriptionForm"
|
||||
:disabled="!valid"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="resetForm"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
<v-dialog v-model="prescriptionModelForm" max-width="1200">
|
||||
<v-card class="pa-3">
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-right cross">
|
||||
<v-btn
|
||||
icon
|
||||
color="transparent"
|
||||
small
|
||||
@click="prescriptionModelForm = false"
|
||||
>
|
||||
<v-icon>rdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="itemsPrescriptions"
|
||||
:search="search"
|
||||
:loading="loading"
|
||||
:page.sync="page"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
@page-count="pageCount = $event"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat :height="30">
|
||||
<v-toolbar-title>Prescriptions</v-toolbar-title>
|
||||
<v-divider
|
||||
class="mx-4"
|
||||
inset
|
||||
vertical
|
||||
></v-divider>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
small
|
||||
@click="selectedItem(item)"
|
||||
>Select</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,258 @@
|
||||
<script setup>
|
||||
import addOrderNote from '@/pages/apps/ecommerce/order/list/addOrderNote.vue';
|
||||
import EditOrderNote from '@/pages/apps/ecommerce/order/list/EditOrderNote.vue';
|
||||
import store from '@/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isAddCustomerDrawerOpen = ref(false)
|
||||
const isEditCustomerDrawerOpen = ref(false)
|
||||
const deleteDialog = ref(false)
|
||||
const props = defineProps({
|
||||
orderData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const handleParentAdded = async (msg) => {
|
||||
if (msg == 'success') {
|
||||
// store.dispatch('updateIsLoading', true)
|
||||
// loadItems({ page: 1, itemsPerPage: itemsPerPage.value, sortBy: [] });
|
||||
// store.dispatch('updateIsLoading', false)
|
||||
}
|
||||
// You can also trigger a toast or snackbar here to show the message
|
||||
// For example, if using Vuetify:
|
||||
// showSnackbar(msg)
|
||||
}
|
||||
const notes = ref([]);
|
||||
const historyNotes = computed(async () => {
|
||||
|
||||
let notesData = props.orderData.appointment_notes;
|
||||
console.log("notesData", notesData);
|
||||
for (let data of notesData) {
|
||||
if (data.note_type == 'Notes') {
|
||||
let dataObject = {}
|
||||
dataObject.note = data.note
|
||||
dataObject.doctor = props.orderData.appointment_details.provider_name;
|
||||
dataObject.date = formatDateDate(data.created_at)
|
||||
dataObject.id = data.id
|
||||
//notes.value.push(dataObject)
|
||||
}
|
||||
}
|
||||
notes.value.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
});
|
||||
console.log("getNotes", notes.value);
|
||||
store.dispatch('updateIsLoading', false)
|
||||
return notes.value
|
||||
});
|
||||
const formatDateDate = (date) => {
|
||||
const messageDate = new Date(date);
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
return messageDate.toLocaleDateString('en-US', options).replace(/\//g, '-');
|
||||
};
|
||||
onMounted(async () => {
|
||||
let notesData = props.orderData.appointment_notes;
|
||||
console.log("notesData", notesData);
|
||||
for (let data of notesData) {
|
||||
if (data.note_type == 'Notes') {
|
||||
let dataObject = {}
|
||||
dataObject.note = data.note
|
||||
dataObject.doctor = props.orderData.appointment_details.provider_name;
|
||||
dataObject.date = formatDateDate(data.created_at)
|
||||
dataObject.id = data.id
|
||||
dataObject.created_by = data.created_by
|
||||
notes.value.push(dataObject)
|
||||
}
|
||||
}
|
||||
notes.value.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
});
|
||||
console.log("getNotes", notes.value);
|
||||
});
|
||||
const addNewOrder = () => {
|
||||
store.dispatch("updateIsLoading", true);
|
||||
router.replace(route.query.to && route.query.to != '/admin/orders' ? String(route.query.to) : '/admin/add-order')
|
||||
store.dispatch("updateIsLoading", false);
|
||||
};
|
||||
|
||||
const getNotes = () =>{
|
||||
|
||||
let notesData = store.getters.getPatientOrderDetail.appointment_notes;
|
||||
notes.value = [];
|
||||
for (let data of notesData) {
|
||||
if (data.note_type == 'Notes') {
|
||||
let dataObject = {}
|
||||
dataObject.note = data.note
|
||||
dataObject.doctor = props.orderData.appointment_details.provider_name;
|
||||
dataObject.date = formatDateDate(data.created_at)
|
||||
dataObject.id = data.id
|
||||
dataObject.created_by = data.created_by
|
||||
notes.value.push(dataObject)
|
||||
}
|
||||
}
|
||||
notes.value.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
});
|
||||
console.log(">>Notes",notes.value);
|
||||
}
|
||||
const editedItem = ref([]);
|
||||
const editedIndex = ref([]);
|
||||
const editItem = async(item) => {
|
||||
isEditCustomerDrawerOpen.value = true;
|
||||
await store.dispatch('GetNoteByID', {
|
||||
id: item.id,
|
||||
})
|
||||
|
||||
editedItem.value = store.getters.getSingleOrderNote
|
||||
console.log(editedItem.value);
|
||||
}
|
||||
const deleteItem = item => {
|
||||
editedIndex.value = notes.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
deleteDialog.value = true
|
||||
}
|
||||
|
||||
const closeDelete = () => {
|
||||
deleteDialog.value = false
|
||||
// editedIndex.value = -1
|
||||
// editedItem.value = { ...defaultItem.value }
|
||||
}
|
||||
|
||||
const deleteItemConfirm = async () => {
|
||||
|
||||
await store.dispatch('DeleteSingleNote',{
|
||||
id: editedItem.value.id
|
||||
})
|
||||
|
||||
closeDelete()
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
|
||||
getNotes();
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8" class="d-flex align-center mb-3">
|
||||
<VBtn color="primary" prepend-icon="ri-add-line" @click="isAddCustomerDrawerOpen = !isAddCustomerDrawerOpen" v-if="$can('read', 'Notes Add')">
|
||||
New Note
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard title="Notes" v-if="notes.length > 0">
|
||||
<VCardText>
|
||||
<VTimeline truncate-line="both" align="start" side="end" line-inset="10" line-color="primary"
|
||||
density="compact" class="v-timeline-density-compact">
|
||||
|
||||
<template v-if="historyNotes">
|
||||
<VTimelineItem dot-color="primary" size="x-small" v-for="(p_note, index) of notes" :key="index">
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">{{ p_note.note }}</span>
|
||||
<span class="app-timeline-meta">{{ p_note.date }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title">{{ p_note.created_by?p_note.created_by:p_note.doctor }}</span>
|
||||
<span class="app-timeline-meta"> <div class="d-flex gap-1">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="editItem(p_note)"
|
||||
v-if="$can('read', 'Notes Edit')"
|
||||
>
|
||||
<VIcon icon="ri-pencil-line" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteItem(p_note)"
|
||||
v-if="$can('read', 'Notes Delete')"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-line" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<!-- <p class="app-timeline-text mb-0">
|
||||
{{ p_note.doctor }}
|
||||
</p> -->
|
||||
</VTimelineItem>
|
||||
</template>
|
||||
|
||||
</VTimeline>
|
||||
</VCardText>
|
||||
|
||||
</VCard>
|
||||
|
||||
<VCard v-else>
|
||||
<VAlert border="start" color="primary" variant="tonal">
|
||||
<div class="text-center">No data found</div>
|
||||
</VAlert>
|
||||
</VCard>
|
||||
<addOrderNote @notes="getNotes" v-model:is-drawer-open="isAddCustomerDrawerOpen" @addedMessage="handleParentAdded" />
|
||||
<EditOrderNote @notes="getNotes" v-model:is-drawer-open="isEditCustomerDrawerOpen" :user-data="store.getters.getSingleOrderNote" @addedMessage="handleParentAdded" />
|
||||
<VDialog
|
||||
v-model="deleteDialog"
|
||||
max-width="500px"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle>
|
||||
Are you sure you want to delete this item?
|
||||
</VCardTitle>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="closeDelete"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
@click="deleteItemConfirm"
|
||||
>
|
||||
OK
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- <VList class="pb-0" lines="two" v-if="historyNotes">
|
||||
<template v-if="notes.length > 0" v-for="(p_note, index) of notes" :key="index">
|
||||
<VListItem class="pb-0" border>
|
||||
<VListItemTitle>
|
||||
<span class="pb-0">{{ p_note.note }}</span>
|
||||
<p class="text-start fs-5 mb-0 pb-0 text-grey">
|
||||
<small> {{ p_note.doctor }}</small>
|
||||
</p>
|
||||
<p class="text-end fs-5 mb-0 pb-0 text-grey">
|
||||
<small> {{ p_note.date }}</small>
|
||||
</p>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VDivider v-if="index !== notes.length - 1" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<VCard>
|
||||
<VAlert border="start" color="rgb(var(--v-theme-yellow))" variant="tonal">
|
||||
<div class="text-center">No data found</div>
|
||||
</VAlert>
|
||||
|
||||
</VCard>
|
||||
</template>
|
||||
</VList> -->
|
||||
</template>
|
@@ -0,0 +1,446 @@
|
||||
<script setup>
|
||||
import AddPrescrption from '@/pages/apps/ecommerce/order/list/AddPrescrption.vue';
|
||||
import EditPrescrption from '@/pages/apps/ecommerce/order/list/EditPrescrption.vue';
|
||||
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from "vuex";
|
||||
const store = useStore();
|
||||
const props = defineProps({
|
||||
orderData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const isAddCustomerDrawerOpen = ref(false)
|
||||
const isEditCustomerDrawerOpen = ref(false)
|
||||
const editedItem = ref()
|
||||
const deleteDialog = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const patient_id = ref()
|
||||
const itemsPrescriptions = ref([]);
|
||||
// const patientId = route.params.patient_id;
|
||||
const prescriptionLoaded = ref(false)
|
||||
const doctorName = ref('');
|
||||
const prescription = computed(async () => {
|
||||
await fetchPrescriptions()
|
||||
return prescriptionLoaded.value ? itemsPrescriptions.value : null
|
||||
})
|
||||
const fetchPrescriptions = async () => {
|
||||
store.dispatch('updateIsLoading', true)
|
||||
await getprescriptionList()
|
||||
doctorName.value = props.orderData.appointment_details.provider_name
|
||||
store.dispatch('updateIsLoading', false)
|
||||
prescriptionLoaded.value = true
|
||||
}
|
||||
const getprescriptionList = async () => {
|
||||
|
||||
let prescriptions = props.orderData.prescription;
|
||||
console.log('edit item',prescriptions)
|
||||
// itemsPrescriptions.value = store.getters.getPrescriptionList
|
||||
for (let data of prescriptions) {
|
||||
let dataObject = {}
|
||||
dataObject.brand = data.brand
|
||||
dataObject.direction_one = data.direction_one
|
||||
dataObject.direction_quantity = data.direction_quantity
|
||||
dataObject.direction_two = data.direction_two
|
||||
dataObject.date = formatDateDate(data.created_at)
|
||||
dataObject.dosage = data.dosage
|
||||
dataObject.from = data.from
|
||||
dataObject.name = data.name
|
||||
dataObject.quantity = data.quantity
|
||||
dataObject.refill_quantity = data.refill_quantity
|
||||
dataObject.status = data.status
|
||||
dataObject.comments = data.comments
|
||||
dataObject.created_by = data.created_by
|
||||
dataObject.prescription_date=data.created_at
|
||||
itemsPrescriptions.value.push(dataObject)
|
||||
}
|
||||
itemsPrescriptions.value.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
});
|
||||
console.log("itemsPrescriptions", itemsPrescriptions.value);
|
||||
|
||||
};
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (dateString) => {
|
||||
return new Date(dateString).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
})
|
||||
}
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'warning'; // Use Vuetify's warning color (typically yellow)
|
||||
case 'shipped':
|
||||
return '#45B8AC'; // Use Vuetify's primary color (typically blue)
|
||||
case 'delivered':
|
||||
return 'green';
|
||||
case 'returned':
|
||||
return 'red';
|
||||
case 'results':
|
||||
return 'blue';
|
||||
default:
|
||||
return 'grey'; // Use Vuetify's grey color for any other status
|
||||
}
|
||||
};
|
||||
onMounted(async () => {
|
||||
let prescriptions = props.orderData.prescription;
|
||||
console.log('props.orderData', props.orderData.prescription)
|
||||
patient_id.value= props.orderData.patient_details.id
|
||||
itemsPrescriptions.value = prescriptions
|
||||
});
|
||||
const editItem = async (item) => {
|
||||
isEditCustomerDrawerOpen.value = true
|
||||
|
||||
await store.dispatch('getOrderDetailPercrptionByID', {
|
||||
id: item.patient_prescription_id,
|
||||
})
|
||||
|
||||
editedItem.value = store.getters.getOrderDetailPrecriptionList
|
||||
console.log(store.getters.getOrderDetailPrecriptionList)
|
||||
// editDialog.value = true
|
||||
}
|
||||
const deleteItem = item => {
|
||||
|
||||
console.log('del', item)
|
||||
deleteDialog.value = true
|
||||
editedItem.value=item
|
||||
}
|
||||
const deleteItemConfirm = async () => {
|
||||
console.log('editedIndex.value', editedItem.value.patient_prescription_id)
|
||||
await store.dispatch('deleteOrderDetailPerscription', {
|
||||
id: editedItem.value.patient_prescription_id
|
||||
})
|
||||
closeDelete()
|
||||
}
|
||||
const closeDelete = async() => {
|
||||
|
||||
deleteDialog.value = false
|
||||
await handlePatientAdded('success')
|
||||
}
|
||||
const handlePatientAdded = async (msg) => {
|
||||
if (msg == 'success') {
|
||||
|
||||
store.dispatch("updateIsLoading", true);
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
let orderData = store.getters.getPatientOrderDetail;
|
||||
itemsPrescriptions.value =orderData.prescription
|
||||
|
||||
store.dispatch("updateIsLoading", false);
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VCol cols="12" md="3" v-if="$can('read', 'Prescription Add')">
|
||||
<VBtn prepend-icon="ri-add-line" @click="isAddCustomerDrawerOpen = !isAddCustomerDrawerOpen">
|
||||
Add Prescription
|
||||
</VBtn>
|
||||
</VCol>
|
||||
<template v-if="itemsPrescriptions.length > 0">
|
||||
<v-row>
|
||||
<v-col v-for="prescription in itemsPrescriptions" :key="prescription.id" cols="12" md="4">
|
||||
|
||||
<v-card class="mx-auto mb-4" elevation="4" hover>
|
||||
<v-img height="200" src="https://cdn.pixabay.com/photo/2016/11/23/15/03/medication-1853400_1280.jpg"
|
||||
class="white--text align-end" gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)">
|
||||
<v-card-title class="text-h5" style="color: #fff;">{{ prescription.prescription_name
|
||||
}}</v-card-title>
|
||||
</v-img>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
|
||||
|
||||
<v-chip :color="getStatusColor(prescription.status)" text-color="white" small class="mr-2">
|
||||
{{ prescription.status }}
|
||||
</v-chip>
|
||||
<IconBtn
|
||||
size="small"
|
||||
|
||||
@click="editItem(prescription)"
|
||||
v-if="$can('read', 'Prescription Edit')"
|
||||
>
|
||||
|
||||
<VIcon icon="ri-pencil-line" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteItem(prescription)"
|
||||
v-if="$can('read', 'Prescription Delete')"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-line" />
|
||||
</IconBtn>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-icon small color="primary">ri-capsule-line</v-icon>
|
||||
<span class="ml-1">Dosage:{{ prescription.dosage }}</span>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-icon small color="primary">ri-medicine-bottle-line</v-icon>
|
||||
<span class="ml-1">Quantity:{{ prescription.quantity }}</span>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-icon small color="primary">ri-calendar-line</v-icon>
|
||||
<span class="ml-1">{{ formatDate(prescription.prescription_date) }}</span>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-icon small color="primary">ri-time-line</v-icon>
|
||||
<span class="ml-1">{{ formatTime(prescription.prescription_date) }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text>
|
||||
More Details
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="prescription.show = !prescription.show">
|
||||
{{ prescription.show ? '' : '' }}
|
||||
<v-icon right color="primary">
|
||||
{{ prescription.show ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-show="prescription.show">
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<strong>Add By:</strong> {{ prescription.created_by}}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>Brand:</strong> {{ prescription.brand }}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>From:</strong> {{ prescription.from }}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>Direction One:</strong> {{ prescription.direction_one }}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>Direction Two:</strong> {{ prescription.direction_two }}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>Refill Quantity:</strong> {{ prescription.refill_quantity }}
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<strong>Direction Quantity:</strong> {{ prescription.direction_quantity }}
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="prescription.comments">
|
||||
<strong>Comments:</strong> {{ prescription.comments }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<VExpansionPanels variant="accordion" style="display: none;">
|
||||
<VExpansionPanel v-for="(item, index) in itemsPrescriptions" :key="index">
|
||||
<div>
|
||||
|
||||
<VExpansionPanelTitle collapse-icon="mdi-chevron-down" expand-icon="mdi-chevron-right"
|
||||
style="margin-left: 0px !important;">
|
||||
<p class=""><b> {{ item.name }}</b>
|
||||
<br />
|
||||
<div class=" pt-2"> {{ doctorName }}</div>
|
||||
<div class=" pt-2">{{ item.date }}</div>
|
||||
</p>
|
||||
|
||||
|
||||
<v-row>
|
||||
|
||||
</v-row>
|
||||
|
||||
<span class="v-expansion-panel-title__icon badge text-warning"
|
||||
v-if="item.status == null">Pending</span>
|
||||
<span class="v-expansion-panel-title__icon badge" v-else>
|
||||
<v-chip :color="getStatusColor(item.status)" label size="small" variant="text">
|
||||
{{ item.status }}
|
||||
</v-chip></span>
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText class="pt-0">
|
||||
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Brand:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p>{{ item.brand }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>From:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p>{{ item.from }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Dosage:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p>{{ item.dosage }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Quantity:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p>{{ item.quantity }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Direction Quantity:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<p>{{ item.direction_quantity }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Direction One:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<p>{{ item.direction_one }} </p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Direction Two:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<p>{{ item.direction_two }} </p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Refill Quantity:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<p>{{ item.refill_quantity }}</p>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Status:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="6">
|
||||
<p v-if="item.status == null" class="text-warning">Pending</p>
|
||||
<p v-else>{{ item.status }}</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class='mt-1'>
|
||||
<v-col cols="12" md="4" sm="6">
|
||||
<p class='heading'><b>Comments:</b></p>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" sm="8">
|
||||
<p>{{ item.comments }} </p>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</VExpansionPanelText>
|
||||
</div>
|
||||
|
||||
|
||||
</VExpansionPanel>
|
||||
<br />
|
||||
</VExpansionPanels>
|
||||
</template>
|
||||
<template v-else="prescriptionLoaded">
|
||||
<VCard>
|
||||
<VCard>
|
||||
<VAlert border="start" color="primary" variant="tonal">
|
||||
<div class="text-center">No data found</div>
|
||||
</VAlert>
|
||||
|
||||
</VCard>
|
||||
</VCard>
|
||||
|
||||
</template>
|
||||
<AddPrescrption v-model:is-drawer-open="isAddCustomerDrawerOpen" :patient-id="patient_id" @patientAdded="handlePatientAdded" />
|
||||
<EditPrescrption v-model:is-drawer-open="isEditCustomerDrawerOpen" :patient-id="patient_id" :user-data="store.getters.getOrderDetailPrecriptionList" @patientAdded="handlePatientAdded" />
|
||||
<VDialog v-model="deleteDialog" max-width="500px">
|
||||
<VCard>
|
||||
<VCardTitle>
|
||||
Are you sure you want to delete this item?
|
||||
</VCardTitle>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn color="error" variant="outlined" @click="closeDelete">
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn color="success" variant="elevated" @click="deleteItemConfirm">
|
||||
OK
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
button.v-expansion-panel-title {
|
||||
background-color: rgb(var(--v-theme-yellow)) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.v-expansion-panel-title.bg-secondary {
|
||||
background-color: rgb(var(--v-theme-yellow)) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.v-expansion-panel-title {
|
||||
background-color: rgb(var(--v-theme-yellow)) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
span.v-expansion-panel-title__icon {
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
span.v-expansion-panel-title__icon {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.v-expansion-panel {
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
</style>
|
135
resources/js/pages/apps/ecommerce/order/list/addOrderNote.vue
Normal file
135
resources/js/pages/apps/ecommerce/order/list/addOrderNote.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
import { VForm } from 'vuetify/components/VForm';
|
||||
import { useStore } from 'vuex';
|
||||
const store = useStore()
|
||||
const router = useRouter();
|
||||
const route = useRoute()
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const order_note = ref()
|
||||
|
||||
|
||||
const emit = defineEmits(['update:isDrawerOpen', 'addedMessage'])
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
const refVForm = ref()
|
||||
const onSubmit = async () => {
|
||||
const { valid } = await refVForm.value.validate()
|
||||
if (valid) {
|
||||
|
||||
await store.dispatch('addOrderNote', {
|
||||
note: order_note.value,
|
||||
orderId:route.params.id
|
||||
})
|
||||
|
||||
|
||||
if (!store.getters.getErrorMsg) {
|
||||
emit('addedMessage', 'success')
|
||||
order_note.value = null
|
||||
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
|
||||
emit('notes', store.getters.getPatientOrderDetail.appointment_notes)
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const resetForm = () => {
|
||||
refVForm.value?.reset()
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
|
||||
// const roleData = ref([]);
|
||||
// const useSortedRole = () => {
|
||||
// const isLoading = ref(false);
|
||||
// const error = ref(null);
|
||||
|
||||
// const sortedRole = computed(() => {
|
||||
// const allOption = { id: '', role: 'Select Any' };
|
||||
// const sortedData = roleData.value.slice().sort((a, b) => {
|
||||
// return a.role.localeCompare(b.role);
|
||||
// });
|
||||
// return [allOption, ...sortedData];
|
||||
// });
|
||||
|
||||
// const fetchRoleData = async () => {
|
||||
// isLoading.value = true;
|
||||
// error.value = null;
|
||||
// try {
|
||||
// await store.dispatch('getAllRolesList');
|
||||
// roleData.value = store.getters.getRolesList || [];
|
||||
// console.log('Fetched Role data:', roleData.value);
|
||||
// } catch (e) {
|
||||
// console.error('Error fetching Role data:', e);
|
||||
// error.value = 'Failed to fetch Role data';
|
||||
// } finally {
|
||||
// isLoading.value = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// onBeforeMount(fetchRoleData);
|
||||
|
||||
// return { sortedRole, isLoading, error, fetchRoleData };
|
||||
// };
|
||||
// const { sortedRole, isLoading, error, fetchRoleData } = useSortedRole();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer :model-value="props.isDrawerOpen" temporary location="end" width="800"
|
||||
@update:model-value="handleDrawerModelValueUpdate">
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection title="Add Note" @cancel="$emit('update:isDrawerOpen', false)" />
|
||||
<VDivider />
|
||||
|
||||
<VCard flat>
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }" class="h-100">
|
||||
<VCardText style="block-size: calc(100vh - 5rem);">
|
||||
<VForm ref="refVForm" @submit.prevent="">
|
||||
<VRow>
|
||||
|
||||
|
||||
|
||||
<VCol cols="12">
|
||||
<VTextarea v-model="order_note" label="Order Note" :rules="[requiredValidator]"
|
||||
placeholder="Note" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<div class="d-flex justify-start">
|
||||
<VBtn type="submit" color="primary" class="me-4" @click="onSubmit">
|
||||
Save
|
||||
</VBtn>
|
||||
<VBtn color="error" variant="outlined" @click="resetForm">
|
||||
Discard
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
</style>
|
374
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal file
374
resources/js/pages/apps/ecommerce/order/list/index.vue
Normal file
@@ -0,0 +1,374 @@
|
||||
<script setup>
|
||||
import mastercard from '@images/logos/mastercard.png';
|
||||
import paypal from '@images/logos/paypal.png';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
const ordersList = ref([]);
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Pending Payment',
|
||||
value: 56,
|
||||
icon: 'ri-calendar-2-line',
|
||||
},
|
||||
{
|
||||
title: 'Completed',
|
||||
value: 12689,
|
||||
icon: 'ri-check-double-line',
|
||||
},
|
||||
{
|
||||
title: 'Refunded',
|
||||
value: 124,
|
||||
icon: 'ri-wallet-3-line',
|
||||
},
|
||||
{
|
||||
title: 'Failed',
|
||||
value: 32,
|
||||
icon: 'ri-error-warning-line',
|
||||
},
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
// Data table Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'Order',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: 'Patient',
|
||||
key: 'patient_name',
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const resolvePaymentStatus = status => {
|
||||
if (status === 1)
|
||||
return {
|
||||
text: 'Paid',
|
||||
color: 'success',
|
||||
}
|
||||
if (status === 2)
|
||||
return {
|
||||
text: 'Pending',
|
||||
color: 'warning',
|
||||
}
|
||||
if (status === 3)
|
||||
return {
|
||||
text: 'Cancelled',
|
||||
color: 'secondary',
|
||||
}
|
||||
if (status === 4)
|
||||
return {
|
||||
text: 'Failed',
|
||||
color: 'error',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStatus = status => {
|
||||
if (status === 'Delivered')
|
||||
return {
|
||||
text: 'Delivered',
|
||||
color: 'success',
|
||||
}
|
||||
if (status === 'Out for Delivery')
|
||||
return {
|
||||
text: 'Out for Delivery',
|
||||
color: 'primary',
|
||||
}
|
||||
if (status === 'Ready to Pickup')
|
||||
return {
|
||||
text: 'Ready to Pickup',
|
||||
color: 'info',
|
||||
}
|
||||
if (status === 'Dispatched')
|
||||
return {
|
||||
text: 'Dispatched',
|
||||
color: 'warning',
|
||||
}
|
||||
}
|
||||
const isLoading = ref(true);
|
||||
onMounted(async () => {
|
||||
store.dispatch("updateIsLoading", true);
|
||||
await store.dispatch("orderList");
|
||||
ordersList.value = store.getters.getOrderList;
|
||||
console.log("ordersList", ordersList.value);
|
||||
isLoading.value = false;
|
||||
});
|
||||
const orders = computed(() => ordersList.value)
|
||||
const totalOrder = computed(() => ordersList.value.length)
|
||||
|
||||
const deleteOrder = async id => {
|
||||
await $api(`/apps/ecommerce/orders/${ id }`, { method: 'DELETE' })
|
||||
fetchOrders()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-2">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, index) in widgetData"
|
||||
:key="index"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs ? 'product-widget' : $vuetify.display.sm ? index < 2 ? 'product-widget' : '' : ''"
|
||||
>
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
<span class="text-base text-capitalize">
|
||||
{{ data.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="42"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="26"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? index !== widgetData.length - 1 : $vuetify.display.smAndUp ? index % 2 === 0 : false"
|
||||
vertical
|
||||
inset
|
||||
length="100"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-sm-space-between align-center justify-start flex-wrap gap-4">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Order"
|
||||
density="compact"
|
||||
style=" max-inline-size: 200px; min-inline-size: 200px;"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
prepend-icon="ri-upload-2-line"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
:headers="headers"
|
||||
:items="orders"
|
||||
:search="searchQuery"
|
||||
item-value="order"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
|
||||
>
|
||||
<!-- Order ID -->
|
||||
<template #item.order="{ item }">
|
||||
<RouterLink :to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.id } }">
|
||||
#{{ item.order_id }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- Date -->
|
||||
<template #item.date="{ item }">
|
||||
{{ new Date(item.date).toDateString() }}
|
||||
</template>
|
||||
|
||||
<!-- Customers -->
|
||||
<template #item.customers="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:variant="!item.avatar.length ? 'tonal' : undefined"
|
||||
:rounded="1"
|
||||
class="me-4"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="font-weight-medium"
|
||||
>{{ avatarText(item.patient_name) }}</span>
|
||||
</VAvatar>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink :to="{ name: 'pages-user-profile-tab', params: { tab: 'profile' } }">
|
||||
<div class="text-high-emphasis font-weight-medium">
|
||||
{{ item.patient_name }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
<span class="text-sm">{{ item.patient_email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Payments -->
|
||||
<template #item.payment="{ item }">
|
||||
<div
|
||||
:class="`text-${resolvePaymentStatus(item.payment)?.color}`"
|
||||
class="d-flex align-center font-weight-medium"
|
||||
>
|
||||
<VIcon
|
||||
size="10"
|
||||
icon="ri-circle-fill"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ resolvePaymentStatus(item.payment)?.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Method -->
|
||||
<template #item.method="{ item }">
|
||||
<div class="d-flex align-start gap-x-2">
|
||||
<img :src="item.method === 'mastercard' ? mastercard : paypal">
|
||||
<div>
|
||||
<VIcon
|
||||
icon="ri-more-line"
|
||||
class="me-2"
|
||||
/>
|
||||
<span v-if="item.method === 'mastercard'">
|
||||
{{ item.methodNumber }}
|
||||
</span>
|
||||
<span v-else>
|
||||
@gmail.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem value="view">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-ecommerce-order-details-id', params: { id: item.order } }"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
View
|
||||
</RouterLink>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
value="delete"
|
||||
@click="deleteOrder(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalOrder) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalOrder / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalOrder / itemsPerPage) ? page = Math.ceil(totalOrder / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#customer-link{
|
||||
&:hover{
|
||||
color: '#000' !important
|
||||
}
|
||||
}
|
||||
|
||||
.product-widget{
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import Notes from "@/pages/apps/ecommerce/order/list//OrderDetailNotes.vue";
|
||||
import OrderDetail from "@/pages/apps/ecommerce/order/list//orders-detail.vue";
|
||||
import Prescription from "@/pages/apps/ecommerce/order/list/OrderDetailPrecrption.vue";
|
||||
const route = useRoute();
|
||||
const isConfirmDialogVisible = ref(false);
|
||||
const isUserInfoEditDialogVisible = ref(false);
|
||||
const isEditAddressDialogVisible = ref(false);
|
||||
const currentTab = ref("tab-1");
|
||||
|
||||
import { useStore } from "vuex";
|
||||
const store = useStore();
|
||||
const orderData = ref(null);
|
||||
const pateintDetail = ref({});
|
||||
const productItems = ref([]);
|
||||
const userTab = ref(null);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
icon: "mdi-clipboard-text-outline",
|
||||
title: "Order Detail",
|
||||
action: 'read',
|
||||
subject: "Order Detail Tab",
|
||||
},
|
||||
{
|
||||
icon: "mdi-note-text-outline",
|
||||
title: "Notes",
|
||||
action: 'read',
|
||||
subject: "Notes Tab",
|
||||
},
|
||||
{
|
||||
icon: "mdi-prescription",
|
||||
title: "Prescriptions",
|
||||
action: 'read',
|
||||
subject: "Prescription Tab",
|
||||
},
|
||||
];
|
||||
const filteredOrders = computed(() => {
|
||||
let filtered = store.getters.getPatientOrderDetail;
|
||||
|
||||
return filtered;
|
||||
});
|
||||
onMounted(async () => {
|
||||
store.dispatch("updateIsLoading", true);
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
orderData.value = store.getters.getPatientOrderDetail;
|
||||
console.log(orderData.value);
|
||||
store.dispatch("updateIsLoading", false);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<VTabs v-model="userTab" grow>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.icon"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
:icon="tab.icon"
|
||||
v-if="$can(tab.action, tab.subject)"
|
||||
/>
|
||||
<span v-if="$can(tab.action, tab.subject)">{{ tab.title }}</span>
|
||||
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="userTab"
|
||||
class="mt-6 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem v-if="$can('read', 'Order Detail Tab')">
|
||||
<OrderDetail />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem v-if="$can('read', 'Notes Tab')">
|
||||
<Notes :order-data="orderData" />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem v-if="$can('read', 'Prescription Tab')">
|
||||
<Prescription :order-data="orderData" />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
1117
resources/js/pages/apps/ecommerce/order/list/order-list-edit.vue
Normal file
1117
resources/js/pages/apps/ecommerce/order/list/order-list-edit.vue
Normal file
File diff suppressed because it is too large
Load Diff
885
resources/js/pages/apps/ecommerce/order/list/orders-detail.vue
Normal file
885
resources/js/pages/apps/ecommerce/order/list/orders-detail.vue
Normal file
@@ -0,0 +1,885 @@
|
||||
<script setup>
|
||||
import avatar1 from "@images/avatars/avatar-1.png";
|
||||
import moment from 'moment-timezone';
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
const route = useRoute();
|
||||
const isConfirmDialogVisible = ref(false);
|
||||
const isUserInfoEditDialogVisible = ref(false);
|
||||
const isEditAddressDialogVisible = ref(false);
|
||||
const scheduleDate = ref('');
|
||||
const scheduleTime = ref('');
|
||||
const isLoading = ref(true);
|
||||
const state = reactive({
|
||||
addLabOrder: false,
|
||||
selectedTestKitId: null,
|
||||
valid: false,
|
||||
testKits: [],
|
||||
item_id: null,
|
||||
labKitList: [],
|
||||
statusItem:null
|
||||
});
|
||||
const getFieldRules = (fieldName, errorMessage) => {
|
||||
if (fieldName) {
|
||||
return [
|
||||
v => !!v || `${errorMessage}`,
|
||||
// Add more validation rules as needed
|
||||
];
|
||||
}
|
||||
|
||||
};
|
||||
const headers = [
|
||||
{
|
||||
title: "Product",
|
||||
key: "title",
|
||||
},
|
||||
{
|
||||
title: "Price",
|
||||
key: "price",
|
||||
},
|
||||
{
|
||||
title: "Quantity",
|
||||
key: "quantity",
|
||||
},
|
||||
{
|
||||
title: "status",
|
||||
key: "status",
|
||||
},
|
||||
|
||||
{
|
||||
title: "Total",
|
||||
key: "total",
|
||||
sortable: false,
|
||||
},
|
||||
];
|
||||
const items = [{ 'key': 'Pending','value':'Pending' }, { 'key':'Processing','value':'Processing' }, { 'key':'Delivered','value':'Delivered' }, { 'key':'Canceled','value':'Canceled' },{ 'key':'Failed','value':'Failed' },{ 'key':'Refunded','value':'Refunded' }]
|
||||
|
||||
|
||||
const headersLab = [
|
||||
{
|
||||
title: "Product",
|
||||
key: "item_name",
|
||||
},
|
||||
{
|
||||
title: "Lab Kit",
|
||||
key: "lab_kit_name",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
title: "Results",
|
||||
key: "result",
|
||||
},
|
||||
|
||||
];
|
||||
const openDialog = (item) => {
|
||||
|
||||
state.item_id = item.id
|
||||
state.addLabOrder = true
|
||||
};
|
||||
const openPdfInNewTab = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
const storeTestKit = async () => {
|
||||
await store.dispatch('saveOrderLabKitBYitems', {
|
||||
lab_kit_id: state.selectedTestKitId,
|
||||
item_id: state.item_id,
|
||||
cart_id: route.params.id
|
||||
})
|
||||
|
||||
console.log('Selected Test Kit:', state.selectedTestKitId);
|
||||
|
||||
state.addLabOrder = false;
|
||||
state.selectedTestKitId = null
|
||||
state.item_id = null
|
||||
|
||||
};
|
||||
const store = useStore();
|
||||
const orderData = ref(null);
|
||||
const pateintDetail = ref({});
|
||||
const productItems = ref([]);
|
||||
const card_number = ref(null)
|
||||
const cvv = ref(null)
|
||||
const expiration_year = ref(null)
|
||||
const expiration_month = ref(null)
|
||||
const filteredOrders = computed(() => {
|
||||
let filtered = store.getters.getPatientOrderDetail;
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const formatDateActviy1 = (date) => {
|
||||
const messageDate = new Date(date);
|
||||
const dayFormatter = new Intl.DateTimeFormat('en-US', { weekday: 'long' });
|
||||
const timeFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
return `${dayFormatter.format(messageDate)} ${timeFormatter.format(messageDate)}`;
|
||||
};
|
||||
const formatDateActviy = (date) => {
|
||||
const messageDate = new Date(date);
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
return messageDate.toLocaleDateString('en-US', options).replace(/\//g, '-');
|
||||
};
|
||||
const formatDate = (date) => {
|
||||
const messageDate = new Date(date);
|
||||
const options = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
|
||||
hour12: false,
|
||||
};
|
||||
const formattedDate = messageDate
|
||||
.toLocaleString("en-US", options)
|
||||
.replace(/\//g, "-");
|
||||
return `${formattedDate}`;
|
||||
};
|
||||
const convertUtcDateTimeToLocal = (utcDate, utcTime, type) => {
|
||||
const utcDateTime = `${utcDate}T${utcTime}Z`; // Use Z to denote UTC timezone explicitly
|
||||
const momentObj = moment.utc(utcDateTime).local(); // Convert UTC to local time
|
||||
|
||||
if (type === 'date') {
|
||||
return momentObj.format('YYYY-MM-DD'); // Return local date
|
||||
} else if (type === 'time') {
|
||||
return momentObj.format('HH:mm:ss'); // Return local time
|
||||
} else {
|
||||
throw new Error("Invalid type specified. Use 'date' or 'time'.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function totalCallDuration(start_time, end_time) {
|
||||
console.log(start_time, end_time);
|
||||
const startMoment = moment(start_time);
|
||||
const endMoment = moment(end_time);
|
||||
|
||||
// Calculate the duration
|
||||
const duration = moment.duration(endMoment.diff(startMoment));
|
||||
const hours = duration.hours();
|
||||
const thours = `${String(hours).padStart(2, "0")}`;
|
||||
const minutes = duration.minutes();
|
||||
const tminutes = `${String(minutes).padStart(2, "0")}`;
|
||||
const seconds = duration.seconds();
|
||||
const tsecond = `${String(seconds).padStart(2, "0")}`;
|
||||
let durationText;
|
||||
if (hours === 0 && minutes === 0) {
|
||||
//for second
|
||||
durationText = ` 00:00:${tsecond}`;
|
||||
} else if (hours === 0 && minutes > 0) {
|
||||
//for minutes
|
||||
durationText = `00:${tminutes}:${tsecond}`;
|
||||
} else if (hours > 0) {
|
||||
//for hours
|
||||
durationText = `${thours}:${tminutes}:${tsecond}`;
|
||||
}
|
||||
const totalDuration = durationText;
|
||||
console.log("Duration:", durationText);
|
||||
// You may need to adjust this function based on your actual data structure
|
||||
// For example, if you have separate first name and last name properties in each appointment object
|
||||
return totalDuration; // For now, just return the first name
|
||||
}
|
||||
const testKits = computed(async () => {
|
||||
//await store.dispatch('getLabKitProductList', {})
|
||||
// console.log(store.getters.getLabOrderProductList)
|
||||
|
||||
//state.testKits = store.getters.getLabOrderProductList
|
||||
});
|
||||
onMounted(async () => {
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
await store.dispatch("orderPaymentDetails", {
|
||||
id: route.params.id,
|
||||
});
|
||||
if(store.getters.getOrderPaymentDetails)
|
||||
{
|
||||
card_number.value = store.getters.getOrderPaymentDetails.card_number
|
||||
cvv.value = store.getters.getOrderPaymentDetails.cvv
|
||||
expiration_year.value = store.getters.getOrderPaymentDetails.expiration_year
|
||||
expiration_month.value = store.getters.getOrderPaymentDetails.expiration_month
|
||||
}
|
||||
orderData.value = store.getters.getPatientOrderDetail;
|
||||
console.log(orderData.value);
|
||||
if (orderData.value.appointment_details) {
|
||||
scheduleDate.value = getConvertedDate(
|
||||
convertUtcTime(
|
||||
orderData.value.appointment_details.appointment_time,
|
||||
orderData.value.appointment_details.appointment_date,
|
||||
orderData.value.appointment_details.timezone
|
||||
)
|
||||
);
|
||||
scheduleTime.value = getConvertedTime(
|
||||
convertUtcTime(
|
||||
orderData.value.appointment_details.appointment_time,
|
||||
orderData.value.appointment_details.appointment_date,
|
||||
orderData.value.appointment_details.timezone
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// let appointmentDate = convertUtcDateTimeToLocal(orderData.value.appointment_details.appointment_date, orderData.value.appointment_details.appointment_time, 'date')
|
||||
// let appointmentTime = convertUtcDateTimeToLocal(orderData.value.appointment_details.appointment_date, orderData.value.appointment_details.appointment_time, 'time')
|
||||
// scheduleDate.value = moment(appointmentDate, "YYYY-MM-DD").format("MMMM DD, YYYY")
|
||||
// scheduleTime.value = moment(appointmentTime, "HH:mm:ss").format("hh:mm A");
|
||||
|
||||
|
||||
await store.dispatch('getOrderLabKit', { cart_id: route.params.id })
|
||||
|
||||
state.labKitList = store.getters.getOrderLabKit
|
||||
console.log('state.testKits', state.labKitList)
|
||||
isLoading.value = false
|
||||
|
||||
});
|
||||
const convertUtcTime = (time, date, timezone) => {
|
||||
const timezones = {
|
||||
"EST": "America/New_York",
|
||||
"CST": "America/Chicago",
|
||||
"MST": "America/Denver",
|
||||
"PST": "America/Los_Angeles",
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
// Get the IANA timezone identifier from the abbreviation
|
||||
const ianaTimeZone = timezones[timezone];
|
||||
|
||||
if (!ianaTimeZone) {
|
||||
throw new Error(`Unknown timezone abbreviation: ${timezone}`);
|
||||
}
|
||||
|
||||
// Combine date and time into a single string
|
||||
const dateTimeString = `${date}T${time}Z`; // Assuming the input date and time are in UTC
|
||||
|
||||
// Create a Date object from the combined string
|
||||
const dateObj = new Date(dateTimeString);
|
||||
|
||||
// Options for the formatter
|
||||
const options = {
|
||||
timeZone: ianaTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
// Create the formatter
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
// Format the date
|
||||
const convertedDateTime = formatter.format(dateObj);
|
||||
|
||||
return convertedDateTime;
|
||||
};
|
||||
const getConvertedTime = (inputDate) => {
|
||||
// Split the input date string into date and time components
|
||||
const [datePart, timePart] = inputDate.split(', ');
|
||||
|
||||
// Split the time component into hours, minutes, and seconds
|
||||
let [hours, minutes, seconds] = timePart.split(':');
|
||||
|
||||
// Convert the hours to an integer
|
||||
hours = parseInt(hours);
|
||||
|
||||
// Determine the period (AM/PM) and adjust the hours if necessary
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12 || 12; // Convert 0 and 12 to 12, and other hours to 1-11
|
||||
|
||||
// Format the time as desired
|
||||
const formattedTime = `${hours.toString().padStart(2, '0')}:${minutes}${period}`;
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
const getConvertedDate = (inputDate) => {
|
||||
// Split the input date string into date and time components
|
||||
const [datePart, timePart] = inputDate.split(', ');
|
||||
|
||||
// Split the date component into month, day, and year
|
||||
const [month, day, year] = datePart.split('/');
|
||||
|
||||
// Create a new Date object from the parsed components
|
||||
const dateObject = new Date(`${year}-${month}-${day}T${timePart}`);
|
||||
|
||||
// Define an array of month names
|
||||
const monthNames = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
|
||||
// Format the date as desired
|
||||
const formattedDate = `${monthNames[dateObject.getMonth()]} ${day}, ${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "warning";
|
||||
case "Shipped":
|
||||
return "info";
|
||||
case "Delivered":
|
||||
return "success";
|
||||
case "Cancelled":
|
||||
return "error";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
|
||||
};
|
||||
const updateStatus = async (item,event) => {
|
||||
console.log(item.id, event)
|
||||
await store.dispatch('updateStatusItem', { order_id: route.params.id, item_id: item.id, status: event })
|
||||
isLoading.value = true
|
||||
await store.dispatch("orderDetailAgent", {
|
||||
id: route.params.id,
|
||||
});
|
||||
orderData.value = store.getters.getPatientOrderDetail;
|
||||
console.log(orderData.value);
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const getStatusColorLabKit = (status) => {
|
||||
switch (status) {
|
||||
case "Ordered":
|
||||
return "info";
|
||||
case "Shipped":
|
||||
return "info";
|
||||
case "Delivered":
|
||||
return "success";
|
||||
case "Cancelled":
|
||||
return "red";
|
||||
case "Waiting For Results":
|
||||
return "error";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
let formattedAmount = amount.toString();
|
||||
|
||||
// Remove '.00' if present
|
||||
if (formattedAmount.includes('.00')) {
|
||||
formattedAmount = formattedAmount.replace('.00', '');
|
||||
}
|
||||
|
||||
// Split into parts for integer and decimal
|
||||
let parts = formattedAmount.split('.');
|
||||
|
||||
// Format integer part with commas
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
// Return formatted number
|
||||
return parts.join('.');
|
||||
}
|
||||
const formatTotalCurrency = (amount) => {
|
||||
let formattedAmount = amount.toString();
|
||||
|
||||
// Remove '.00' if present
|
||||
// if (formattedAmount.includes('.00')) {
|
||||
// formattedAmount = formattedAmount.replace('.00', '');
|
||||
// }
|
||||
|
||||
// Split into parts for integer and decimal
|
||||
let parts = formattedAmount.split('.');
|
||||
|
||||
// Format integer part with commas
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
// Return formatted number
|
||||
return parts.join('.');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-y-4 mb-6">
|
||||
<div>
|
||||
<div class="d-flex gap-2 align-center mb-2 flex-wrap">
|
||||
<h5 class="text-h5">Order #{{ route.params.id }}</h5>
|
||||
<div class="d-flex gap-x-2">
|
||||
|
||||
<!-- <VChip variant="tonal" color="success" size="small">
|
||||
Paid
|
||||
</VChip>
|
||||
<VChip variant="tonal" color="info" size="small">
|
||||
Ready to Pickup
|
||||
</VChip> -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-body-1"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VRow v-if="filteredOrders">
|
||||
<VCol cols="12" md="8">
|
||||
<!-- 👉 Order Details -->
|
||||
<VCard class="mb-6">
|
||||
<VCardItem>
|
||||
<template #title>
|
||||
<h5>Order Details</h5>
|
||||
|
||||
</template>
|
||||
</VCardItem>
|
||||
<div class="table-container">
|
||||
<VDataTable :headers="headers" :items="filteredOrders.order_items.items"
|
||||
item-value="productName" class="text-no-wrap " :loading="isLoading">
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex gap-x-3">
|
||||
<VAvatar size="34" variant="tonal" :image="item.image_url" rounded />
|
||||
|
||||
<div class="d-flex flex-column text-left">
|
||||
<h5 style="margin-bottom: 0px; font-size: 0.83em; white-space: normal;word-break: break-word;" >
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
|
||||
<span class="text-sm text-start align-self-start">
|
||||
{{ item.list_sub_title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.price="{ item }">
|
||||
<span>${{ item.price }}</span>
|
||||
</template>
|
||||
|
||||
|
||||
<template #item.status="{ item }" >
|
||||
<span>
|
||||
|
||||
<VSelect
|
||||
:items="items"
|
||||
v-model="item.status"
|
||||
label="Status"
|
||||
placeholder="Select Status"
|
||||
density="compact"
|
||||
style="width: 150px;"
|
||||
item-title="key"
|
||||
item-value="value"
|
||||
@update:model-value="updateStatus(item,$event)"
|
||||
/>
|
||||
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item.total="{ item }">
|
||||
|
||||
<span> ${{ parseFloat(item.price * item.quantity).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}) }} </span>
|
||||
</template>
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
${{
|
||||
formatTotalCurrency(parseFloat(
|
||||
filteredOrders.order_items
|
||||
.total_amount
|
||||
).toFixed(2))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shipping fee:</td>
|
||||
<td class="font-weight-medium">
|
||||
${{
|
||||
parseFloat(
|
||||
filteredOrders.order_items
|
||||
.total_shipping_cost
|
||||
).toFixed(2)
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="font-weight-medium">
|
||||
Total:
|
||||
</td>
|
||||
<td class="font-weight-medium">
|
||||
${{
|
||||
formatTotalCurrency(parseFloat(
|
||||
filteredOrders.order_items
|
||||
.total
|
||||
).toFixed(2))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<v-dialog v-model="state.addLabOrder" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>Add Lab Kit</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="form" v-model="state.valid" class="mt-1">
|
||||
<v-row v-if="testKits">
|
||||
<v-col cols="12" md="12">
|
||||
<v-autocomplete label="Test Kit" v-model="state.selectedTestKitId"
|
||||
style="column-gap: 0px;" :items="state.testKits" item-title="name"
|
||||
item-value="id"
|
||||
:rules="getFieldRules('Test Kit', 'Test Kit is required')"></v-autocomplete>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="state.addLabOrder = false">Cancel</v-btn>
|
||||
<v-btn color="primary" @click="storeTestKit" :disabled="!state.valid">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!-- 👉 Shipping Activity -->
|
||||
|
||||
|
||||
<VCard title="Lab Kits" v-if="state.labKitList.length > 0" class="mb-6">
|
||||
<VCardText>
|
||||
<div class="table-container">
|
||||
<VDataTable :headers="headersLab" :loading="isLoading" :items="state.labKitList"
|
||||
class="text-no-wrap ">
|
||||
<template #item.item_name="{ item }">
|
||||
<div class="d-flex gap-x-3">
|
||||
|
||||
<div class="d-flex flex-column align-center">
|
||||
<h5 style="margin-bottom: 0px;">
|
||||
{{ item.item_name }}
|
||||
</h5>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.lab_kit_name="{ item }">
|
||||
<div class="d-flex gap-x-3">
|
||||
|
||||
<div class="d-flex flex-column align-center">
|
||||
<h5 style="margin-bottom: 0px;">
|
||||
{{ item.lab_kit_name }}
|
||||
</h5>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<span>
|
||||
<VChip variant="tonal" :color="getStatusColorLabKit(item.status)" size="small">
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</span>
|
||||
</template>
|
||||
<template #item.result="{ item }">
|
||||
<span v-if="item.result">
|
||||
<a href="#" @click="openPdfInNewTab(item.result)" target="_blank"
|
||||
class="custom-link">
|
||||
<div class="d-inline-flex align-center">
|
||||
<img :src="pdf" height="20" class="me-2" alt="img">
|
||||
<span class="app-timeline-text font-weight-medium">
|
||||
results.pdf
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</span>
|
||||
<span v-else>
|
||||
Waiting For Result
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCard title="Shipping Activity" class="mb-6" >
|
||||
<VCardText>
|
||||
<VTimeline truncate-line="both" align="start" side="end" line-inset="10" line-color="primary"
|
||||
density="compact" class="v-timeline-density-compact">
|
||||
<VTimelineItem dot-color="primary" size="x-small"
|
||||
v-for="item in filteredOrders.items_activity" :key="item.id">
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="app-timeline-title"> {{ item.note }}</span>
|
||||
<span class="app-timeline-meta">{{ formatDateActviy(item.created_at) }}</span>
|
||||
</div>
|
||||
<p class="app-timeline-text mb-0">
|
||||
{{ item.item_name }} {{ item.short_description }}
|
||||
</p>
|
||||
</VTimelineItem>
|
||||
|
||||
|
||||
</VTimeline>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<VCard class="mb-6" v-if="filteredOrders.appointment_details">
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-6">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
<v-icon class="mr-2" color="primary">ri-calendar-event-line</v-icon>
|
||||
Appointment Details
|
||||
</div>
|
||||
</div>
|
||||
<div class="appointment-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Appointment At:</span>
|
||||
<span class="detail-value">{{ scheduleDate + ' ' + scheduleTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item"
|
||||
v-if="filteredOrders.appointment_details.start_time && filteredOrders.appointment_details.end_time">
|
||||
<span class="detail-label">Start Time:</span>
|
||||
<span class="detail-value">{{
|
||||
formatDate(filteredOrders.appointment_details.start_time)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="detail-item"
|
||||
v-if="filteredOrders.appointment_details.start_time && filteredOrders.appointment_details.end_time">
|
||||
<span class="detail-label">End Time:</span>
|
||||
<span class="detail-value">{{
|
||||
formatDate(filteredOrders.appointment_details.end_time)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="detail-item"
|
||||
v-if="filteredOrders.appointment_details.start_time && filteredOrders.appointment_details.end_time">
|
||||
<span class="detail-label">Duration:</span>
|
||||
<span class="detail-value">{{
|
||||
totalCallDuration(filteredOrders.appointment_details.start_time,
|
||||
filteredOrders.appointment_details.end_time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 👉 Customer Details -->
|
||||
<VCard class="mb-6" v-if="filteredOrders.patient_details">
|
||||
<VCardText class="d-flex flex-column gap-y-6">
|
||||
<h3>Patient Details</h3>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar :image="avatar1" class="me-3" />
|
||||
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
{{
|
||||
filteredOrders.patient_details.first_name + ' ' +
|
||||
filteredOrders.patient_details.last_name
|
||||
}}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="display: none;">
|
||||
<VAvatar variant="tonal" color="success" class="me-3" style="display: none;">
|
||||
<VIcon icon="ri-shopping-cart-line" />
|
||||
</VAvatar>
|
||||
|
||||
<h4 style="display: none;">
|
||||
{{ filteredOrders.order_items.total_products }}
|
||||
Products
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<div class="d-flex justify-space-between gap-1 text-body-2">
|
||||
<h5>Contact Info</h5>
|
||||
</div>
|
||||
|
||||
<span>Email:
|
||||
{{ filteredOrders.patient_details.email }}</span>
|
||||
<span>Mobile:
|
||||
{{ filteredOrders.patient_details.phone_no }}</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Card Details -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-6">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
<v-icon class="mr-2" color="primary">ri-bank-card-fill</v-icon>
|
||||
Card Details
|
||||
</div>
|
||||
<!-- <span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="
|
||||
isEditAddressDialogVisible =
|
||||
!isEditAddressDialogVisible
|
||||
"
|
||||
>Edit</span
|
||||
> -->
|
||||
</div>
|
||||
<div>
|
||||
Card: {{ card_number }}
|
||||
<br />
|
||||
Expiry: {{ expiration_month }} / {{ expiration_year }}
|
||||
<br />
|
||||
CVV: {{ cvv }}
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Shipping Address -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-6">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
<v-icon class="mr-2" color="primary">ri-truck-line</v-icon>
|
||||
Shipping Address
|
||||
</div>
|
||||
<!-- <span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="
|
||||
isEditAddressDialogVisible =
|
||||
!isEditAddressDialogVisible
|
||||
"
|
||||
>Edit</span
|
||||
> -->
|
||||
</div>
|
||||
<div>
|
||||
{{ filteredOrders.order_details.shipping_address1 }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.shipping_city }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.shipping_state }},
|
||||
{{ filteredOrders.order_details.shipping_zipcode }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.shipping_country }}
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Billing Address -->
|
||||
<VCard style="display: none;">
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between gap-1 mb-3">
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">
|
||||
Billing Address
|
||||
</div>
|
||||
<!-- <span
|
||||
class="text-base text-primary font-weight-medium cursor-pointer"
|
||||
@click="
|
||||
isEditAddressDialogVisible =
|
||||
!isEditAddressDialogVisible
|
||||
"
|
||||
>Edit</span
|
||||
> -->
|
||||
</div>
|
||||
<div>
|
||||
{{ filteredOrders.order_details.billing_address1 }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.billing_city }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.billing_state }},
|
||||
{{ filteredOrders.order_details.billing_zipcode }}
|
||||
<br />
|
||||
{{ filteredOrders.order_details.billing_country }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-6">
|
||||
<h6 class="text-h6 mb-1">Mastercard</h6>
|
||||
<div class="text-base">Card Number: ******4291</div>
|
||||
</div> -->
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- <ConfirmDialog
|
||||
v-model:isDialogVisible="isConfirmDialogVisible"
|
||||
confirmation-question="Are you sure to cancel your Order?"
|
||||
cancel-msg="Order cancelled!!"
|
||||
cancel-title="Cancelled"
|
||||
confirm-msg="Your order cancelled successfully."
|
||||
confirm-title="Cancelled!"
|
||||
/> -->
|
||||
|
||||
<!-- <UserInfoEditDialog
|
||||
v-model:isDialogVisible="isUserInfoEditDialogVisible"
|
||||
/>
|
||||
|
||||
<AddEditAddressDialog
|
||||
v-model:isDialogVisible="isEditAddressDialogVisible"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.appointment-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: bold;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
/* Width of the scrollbar */
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
/* Color of the track */
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
/* Color of the handle */
|
||||
border-radius: 5px;
|
||||
/* Roundness of the handle */
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
/* Color of the handle on hover */
|
||||
}
|
||||
</style>
|
626
resources/js/pages/apps/ecommerce/order/list/orders-list-new.vue
Normal file
626
resources/js/pages/apps/ecommerce/order/list/orders-list-new.vue
Normal file
@@ -0,0 +1,626 @@
|
||||
<script setup>
|
||||
|
||||
import API from '@/api';
|
||||
import { ADMIN_GET_ORDER_API } from '@/constants';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from "vuex";
|
||||
const date = ref();
|
||||
const router = useRouter();
|
||||
const route = useRoute()
|
||||
const store = useStore();
|
||||
const ordersList = ref([]);
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Total Orders',
|
||||
value: 0,
|
||||
icon: 'ri-calendar-2-line',
|
||||
key_value: 'total_order'
|
||||
},
|
||||
{
|
||||
title: 'Total Appointment Orders',
|
||||
value: 0,
|
||||
icon: 'ri-check-double-line',
|
||||
key_value: 'total_appointment_order'
|
||||
},
|
||||
{
|
||||
title: ' Without Appointment Orders',
|
||||
value: 0,
|
||||
icon: 'ri-wallet-3-line',
|
||||
key_value: 'total_appointment_order_without'
|
||||
},
|
||||
{
|
||||
title: 'Completed Meetings Orders',
|
||||
value: 0,
|
||||
icon: 'ri-error-warning-line',
|
||||
key_value: 'completedMeetings'
|
||||
},
|
||||
])
|
||||
const searchQuery = ref('')
|
||||
// const search = ref('')
|
||||
|
||||
// Data table options
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const isLoading = ref(true);
|
||||
// Data table Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'Order',
|
||||
key: 'order_id',
|
||||
searchable:false
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
title: 'Patient',
|
||||
key: 'patient_name',
|
||||
searchable:true
|
||||
},
|
||||
{
|
||||
title: 'Meeting Date',
|
||||
key: 'appointment_date',
|
||||
searchable:false
|
||||
},
|
||||
{
|
||||
title: 'Meeting Time',
|
||||
key: 'appointment_time',
|
||||
searchable:false
|
||||
},
|
||||
{
|
||||
title: 'Total Amount',
|
||||
key: 'total_amount',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
searchable:false
|
||||
},
|
||||
]
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const resolvePaymentStatus = status => {
|
||||
if (status === 1)
|
||||
return {
|
||||
text: 'Paid',
|
||||
color: 'success',
|
||||
}
|
||||
if (status === 2)
|
||||
return {
|
||||
text: 'Pending',
|
||||
color: 'warning',
|
||||
}
|
||||
if (status === 3)
|
||||
return {
|
||||
text: 'Cancelled',
|
||||
color: 'secondary',
|
||||
}
|
||||
if (status === 4)
|
||||
return {
|
||||
text: 'Failed',
|
||||
color: 'error',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStatusVariant = (status) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'completed':
|
||||
return { color: 'success', text: 'Completed' }
|
||||
case 'scheduled':
|
||||
return { color: 'primary', text: 'Scheduled' }
|
||||
case 'cancelled':
|
||||
return { color: 'error', text: 'Cancelled' }
|
||||
default:
|
||||
return { color: 'warning', text: 'Pending' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ordersGetList = computed(() => {
|
||||
|
||||
return ordersList.value.map((history) => ({
|
||||
...history,
|
||||
//appointment_date: getConvertedDate(convertUtcTime(history.appointment_time, history.appointment_date, history.timezone)),
|
||||
//appointment_time: getConvertedTime(convertUtcTime(history.appointment_time, history.appointment_date, history.timezone)),
|
||||
|
||||
// appointment_date: changeFormat(history.appointment_date),
|
||||
// appointment_time: convertUtcDateTimeToLocal(history.appointment_date, history.appointment_time, 'time'),
|
||||
// start_time: changeDateFormat(history.start_time),
|
||||
// end_time: changeDateFormat(history.end_time),
|
||||
// duration: totalCallDuration(history.start_time, history.end_time),
|
||||
}));
|
||||
// isLoading.value - false
|
||||
// ordersList.value.sort((a, b) => {
|
||||
// return b.id - a.id;
|
||||
// });
|
||||
// return ordersList.value
|
||||
});
|
||||
onMounted(async () => {
|
||||
// setDateRange();
|
||||
store.dispatch("updateIsLoading", true);
|
||||
// await store.dispatch("orderList");
|
||||
// ordersList.value = store.getters.getOrderList;
|
||||
// ordersList.value.sort((a, b) => {
|
||||
// return b.id - a.id;
|
||||
// });
|
||||
await store.dispatch("orderCount");
|
||||
let orderCount = store.getters.getOrderCount
|
||||
for (let data of widgetData.value) {
|
||||
if (data.key_value == 'total_order') {
|
||||
data.value = orderCount[data.key_value]
|
||||
}
|
||||
if (data.key_value == 'total_appointment_order') {
|
||||
data.value = orderCount[data.key_value]
|
||||
}
|
||||
if (data.key_value == 'total_appointment_order_without') {
|
||||
data.value = orderCount[data.key_value]
|
||||
}
|
||||
if (data.key_value == 'completedMeetings') {
|
||||
data.value = orderCount[data.key_value]
|
||||
}
|
||||
console.log(orderCount, orderCount[data.key_value])
|
||||
}
|
||||
console.log("ordersList", orderCount);
|
||||
loadItems({ page: 1, itemsPerPage: itemsPerPage.value, sortBy: [] });
|
||||
isLoading.value = false;
|
||||
});
|
||||
const setDateRange = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
// Set the date range
|
||||
startRangeDate.value = formattedDate(startOfMonth);
|
||||
endRangeDate.value = formattedDate(today);
|
||||
date.value = [startOfMonth, today];
|
||||
// console.log("mmm>>>>",date.value, startOfMonth, today);
|
||||
};
|
||||
const formattedDate = (originalDate) => {
|
||||
// Convert the original date string into a Date object
|
||||
const date = new Date(originalDate);
|
||||
|
||||
// Extract year, month, and day
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
// Return formatted date in YYYY-MM-DD format
|
||||
return `${month}-${day}-${year}`;
|
||||
};
|
||||
const convertUtcTime = (time, date, timezone) => {
|
||||
const timezones = {
|
||||
"EST": "America/New_York",
|
||||
"CST": "America/Chicago",
|
||||
"MST": "America/Denver",
|
||||
"PST": "America/Los_Angeles",
|
||||
"PST": "Asia/Karachi"
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
// Get the IANA timezone identifier from the abbreviation
|
||||
let ianaTimeZone = timezones[timezone];
|
||||
|
||||
if (!ianaTimeZone) {
|
||||
// throw new Error(`Unknown timezone abbreviation: ${timezone}`);
|
||||
timezone = 'PST'
|
||||
ianaTimeZone = timezones[timezone];
|
||||
}
|
||||
|
||||
// Combine date and time into a single string
|
||||
const dateTimeString = `${date}T${time}Z`; // Assuming the input date and time are in UTC
|
||||
|
||||
// Create a Date object from the combined string
|
||||
const dateObj = new Date(dateTimeString);
|
||||
|
||||
// Options for the formatter
|
||||
const options = {
|
||||
timeZone: ianaTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
// Create the formatter
|
||||
const formatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
// Format the date
|
||||
const convertedDateTime = formatter.format(dateObj);
|
||||
|
||||
return convertedDateTime;
|
||||
};
|
||||
const getConvertedTime = (inputDate) => {
|
||||
// Split the input date string into date and time components
|
||||
const [datePart, timePart] = inputDate.split(', ');
|
||||
|
||||
// Split the time component into hours, minutes, and seconds
|
||||
let [hours, minutes, seconds] = timePart.split(':');
|
||||
|
||||
// Convert the hours to an integer
|
||||
hours = parseInt(hours);
|
||||
|
||||
// Determine the period (AM/PM) and adjust the hours if necessary
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12 || 12; // Convert 0 and 12 to 12, and other hours to 1-11
|
||||
|
||||
// Format the time as desired
|
||||
const formattedTime = `${hours.toString().padStart(2, '0')}:${minutes}${period}`;
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
const getConvertedDate = (inputDate) => {
|
||||
// Split the input date string into date and time components
|
||||
const [datePart, timePart] = inputDate.split(', ');
|
||||
|
||||
// Split the date component into month, day, and year
|
||||
const [month, day, year] = datePart.split('/');
|
||||
|
||||
// Create a new Date object from the parsed components
|
||||
const dateObject = new Date(`${year}-${month}-${day}T${timePart}`);
|
||||
|
||||
// Define an array of month names
|
||||
const monthNames = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
|
||||
// Format the date as desired
|
||||
const formattedDate = `${monthNames[dateObject.getMonth()]} ${day}, ${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
function changeFormat(dateFormat) {
|
||||
const dateParts = dateFormat.split('-'); // Assuming date is in yyyy-mm-dd format
|
||||
const year = parseInt(dateParts[0]);
|
||||
const month = String(dateParts[1]).padStart(2, '0'); // Pad single-digit months with leading zero
|
||||
const day = String(dateParts[2]).padStart(2, '0'); // Pad single-digit days with leading zero
|
||||
|
||||
// Create a new Date object with the parsed values
|
||||
const date = new Date(year, month - 1, day); // Month is zero-based in JavaScript Date object
|
||||
|
||||
// Format the date as mm-dd-yyyy
|
||||
const formattedDate = month + '-' + day + '-' + date.getFullYear();
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
const viewOrder = (orderId) => {
|
||||
router.push({ name: "admin-order-detail", params: { id: orderId } });
|
||||
};
|
||||
const formatDate = (date) => {
|
||||
const messageDate = new Date(date);
|
||||
const options = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
};
|
||||
const formattedDate = messageDate
|
||||
.toLocaleString("en-US", options)
|
||||
.replace(/\//g, "-")
|
||||
.replace(/,/, "");
|
||||
return `${formattedDate}`;
|
||||
};
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "orange";
|
||||
case "Shipped":
|
||||
return "blue";
|
||||
case "Delivered":
|
||||
return "green";
|
||||
case "Cancelled":
|
||||
return "red";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
const serverItems = ref([]);
|
||||
const loading = ref(true);
|
||||
const totalItems = ref(0);
|
||||
const search = ref();
|
||||
const itemsPerPage = ref(30);
|
||||
const status = ref('All');
|
||||
const loadItems = debounce(async ({ page, itemsPerPage, sortBy }) => {
|
||||
// if(status.value = 'All'){
|
||||
// status.value = 'all'
|
||||
// }
|
||||
const payload = {
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
filters: {
|
||||
from_date:startRangeDate.value ? startRangeDate.value: 'all',
|
||||
to_date: endRangeDate.value ?endRangeDate.value : 'all',
|
||||
status:status.value.toLowerCase(),
|
||||
},
|
||||
search: search.value,
|
||||
}
|
||||
console.log("records", page, itemsPerPage, sortBy, ADMIN_GET_ORDER_API);
|
||||
loading.value = true;
|
||||
const data = await API.getDataTableRecord(ADMIN_GET_ORDER_API, payload, headers);
|
||||
console.log('patientData', data);
|
||||
serverItems.value = data.items;
|
||||
totalItems.value = data.total;
|
||||
loading.value = false;
|
||||
|
||||
}, 500);
|
||||
const addNewOrder = () => {
|
||||
store.dispatch("updateIsLoading", true);
|
||||
router.replace(route.query.to && route.query.to != '/admin/orders' ? String(route.query.to) : '/admin/add-order')
|
||||
store.dispatch("updateIsLoading", false);
|
||||
};
|
||||
|
||||
const startRangeDate = ref();
|
||||
const endRangeDate = ref();
|
||||
const changeDateRange = async () => {
|
||||
console.log('changed date', date.value, 'type:', typeof date.value);
|
||||
|
||||
try {
|
||||
const dateString = typeof date.value === 'string' ? date.value : '';
|
||||
const [startDate, endDate] = dateString.split(" to ");
|
||||
|
||||
if (startDate && endDate) {
|
||||
startRangeDate.value = formattedDate(startDate);
|
||||
endRangeDate.value = formattedDate(endDate);
|
||||
loadItems({ page: 1, itemsPerPage: itemsPerPage.value, sortBy: [] });
|
||||
// await store.dispatch('getAdminAnalyticsOverview', {
|
||||
// start_date: startDate,
|
||||
// end_date: endDate,
|
||||
// });
|
||||
// analyticsData.value = store.getters.getAnalyticsOverview;
|
||||
} else {
|
||||
console.warn('Invalid date range');
|
||||
// Handle invalid date range
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing date range:', error);
|
||||
// Handle the error appropriately
|
||||
}
|
||||
};
|
||||
const onStateChange = () =>{
|
||||
loadItems({ page: 1, itemsPerPage: itemsPerPage.value, sortBy: [] });
|
||||
};
|
||||
const reset = () => {
|
||||
date.value = '';
|
||||
startRangeDate.value = 'all';
|
||||
endRangeDate.value = 'all';
|
||||
status.value = 'All';
|
||||
search.value= '';
|
||||
loadItems({ page: 1, itemsPerPage: itemsPerPage.value, sortBy: [] });
|
||||
}
|
||||
const formatOrderId = (id) => {
|
||||
if (id >= 1 && id <= 9) {
|
||||
return id.toString().padStart(4, '0');
|
||||
} else if (id >= 10 && id <= 99) {
|
||||
return id.toString().padStart(4, '0');
|
||||
} else if (id >= 100 && id <= 999) {
|
||||
return id.toString().padStart(4, '0');
|
||||
} else {
|
||||
return id; // or handle cases for IDs outside these ranges
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-2">
|
||||
<VRow>
|
||||
<template v-for="(data, index) in widgetData" :key="index">
|
||||
<VCol cols="12" sm="6" md="3" class="px-6">
|
||||
<div class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs ? 'product-widget' : $vuetify.display.sm ? index < 2 ? 'product-widget' : '' : ''">
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
<span class="text-base text-capitalize">
|
||||
{{ data.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VAvatar variant="tonal" rounded size="42">
|
||||
<VIcon :icon="data.icon" size="26" />
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? index !== widgetData.length - 1 : $vuetify.display.smAndUp ? index % 2 === 0 : false"
|
||||
vertical inset length="100" />
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VRow>
|
||||
|
||||
<VCol cols="12" md="12">
|
||||
<VCard title="Orders">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<AppDateTimePicker
|
||||
v-model="date"
|
||||
label="Date Range"
|
||||
:config="{ mode: 'range' }"
|
||||
density="compact"
|
||||
@change="changeDateRange()"
|
||||
>
|
||||
|
||||
</AppDateTimePicker>
|
||||
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="2" class="px-0">
|
||||
<VAutocomplete v-model="status" label="Status" placeholder="Status" density="compact"
|
||||
:items="['All', 'Pending', 'Recevied', 'Complete','Shipped','Transit']" @update:model-value="onStateChange" />
|
||||
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VTextField v-model="search" placeholder="Search Order" density="compact"
|
||||
style="max-inline-size: 200px; min-inline-size: 200px;" />
|
||||
|
||||
</VCol>
|
||||
<!-- <VCol cols="12" md="1">
|
||||
|
||||
</VCol> -->
|
||||
<VCol
|
||||
cols="12" class="pl-0"
|
||||
|
||||
md="3"
|
||||
>
|
||||
<VBtn @click="reset()" class="mr-2">Reset</VBtn>
|
||||
|
||||
<VBtn color="primary" @click="addNewOrder" v-if="$can('read', 'Order Add')">New Order</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<!-- <VDataTable :loading="isLoading" :headers="headers" :items="ordersGetList"
|
||||
:search="searchQuery" :items-per-page="5" class="text-no-wrap"> -->
|
||||
<v-data-table-server v-model:items-per-page="itemsPerPage" :headers="headers" :items="serverItems"
|
||||
:items-length="totalItems" :loading="loading" :search="search" :item-value="name"
|
||||
@update:options="loadItems">
|
||||
|
||||
<!-- full name -->
|
||||
<template #item.order_id="{ item }">
|
||||
<RouterLink :to="{ name: 'admin-order-detail', params: { id: item.order_id } }">
|
||||
{{ formatOrderId(item.order_id) }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template #item.total_amount="{ item }"> ${{ parseFloat(item.total_amount + item.order_total_shipping).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}) }}
|
||||
<!-- ${{ parseFloat(item.order_total_amount +
|
||||
item.order_total_shipping).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}) }} -->
|
||||
</template>
|
||||
<template #item.patient_name="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar size="32" :color="item.profile_picture ? '' : 'primary'"
|
||||
:class="item.profile_picture ? '' : 'v-avatar-light-bg primary--text'"
|
||||
:variant="!item.profile_picture ? 'tonal' : undefined">
|
||||
<VImg v-if="item.profile_picture" :src="item.profile_picture" />
|
||||
<span v-else>{{ avatarText(item.patient_name) }}</span>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column ms-3">
|
||||
<RouterLink :to="{ name: 'admin-patient-profile', params: { id: item.patient_id } }">
|
||||
<div class=" font-weight-medium">
|
||||
{{ item.patient_name }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
<small>{{ item.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template #item.date="{ item }">
|
||||
<span>
|
||||
|
||||
{{ formatDate(item.created_at) }}
|
||||
|
||||
</span>
|
||||
</template>
|
||||
<template #item.appointment_date="{ item }">
|
||||
<div v-if="item.appointment_date">
|
||||
{{ changeFormat(item.appointment_date) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template #item.appointment_time="{ item }">
|
||||
<div v-if="item.appointment_time">
|
||||
|
||||
{{ getConvertedTime(convertUtcTime(item.appointment_time, item.appointment_date, item.timezone)) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<span>
|
||||
<VChip variant="tonal" :color="getStatusColor(item.status)" size="small">
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- status -->
|
||||
|
||||
<template #item.appointment_status="{ item }">
|
||||
<VChip :color="resolveStatusVariant(item.appointment_status).color" class="font-weight-medium"
|
||||
size="small" :class="{ 'blink-status': item.appointment_status.toLowerCase() !== 'completed' }">
|
||||
{{ resolveStatusVariant(item.appointment_status).text }}
|
||||
</VChip>
|
||||
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-line" />
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem value="view" v-if="$can('read', 'Order Detail Tab')">
|
||||
<RouterLink :to="{ name: 'admin-order-detail', params: { id: item.order_id } }"
|
||||
class="text-high-emphasis">
|
||||
View
|
||||
</RouterLink>
|
||||
</VListItem>
|
||||
<VListItem value="view" v-if="$can('read', 'Order Edit')">
|
||||
<RouterLink :to="{ name: 'admin-order-edit', params: { id: item.order_id } }"
|
||||
class="text-high-emphasis">
|
||||
Edit
|
||||
</RouterLink>
|
||||
</VListItem>
|
||||
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
<!-- </VDataTable> -->
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#customer-link {
|
||||
&:hover {
|
||||
color: '#000' !important
|
||||
}
|
||||
}
|
||||
|
||||
.product-widget {
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
688
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal file
688
resources/js/pages/apps/ecommerce/product/add/index.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<script setup>
|
||||
import {
|
||||
useDropZone,
|
||||
useFileDialog,
|
||||
useObjectUrl,
|
||||
} from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const optionCounter = ref(1)
|
||||
const dropZoneRef = ref()
|
||||
const fileData = ref([])
|
||||
const { open, onChange } = useFileDialog({ accept: 'image/*' })
|
||||
function onDrop(DroppedFiles) {
|
||||
DroppedFiles?.forEach(file => {
|
||||
if (file.type.slice(0, 6) !== 'image/') {
|
||||
alert('Only image files are allowed')
|
||||
|
||||
return
|
||||
}
|
||||
fileData.value.push({
|
||||
file,
|
||||
url: useObjectUrl(file).value ?? '',
|
||||
})
|
||||
})
|
||||
}
|
||||
onChange(selectedFiles => {
|
||||
if (!selectedFiles)
|
||||
return
|
||||
for (const file of selectedFiles) {
|
||||
fileData.value.push({
|
||||
file,
|
||||
url: useObjectUrl(file).value ?? '',
|
||||
})
|
||||
}
|
||||
})
|
||||
useDropZone(dropZoneRef, onDrop)
|
||||
|
||||
const content = ref(`<p>
|
||||
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. That's it. It's probably too much for real minimalists though.
|
||||
</p>
|
||||
<p>
|
||||
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
|
||||
</p>`)
|
||||
|
||||
const activeTab = ref('Restock')
|
||||
|
||||
const shippingList = [
|
||||
{
|
||||
desc: 'You\'ll be responsible for product delivery.Any damage or delay during shipping may cost you a Damage fee',
|
||||
title: 'Fulfilled by Seller',
|
||||
value: 'Fulfilled by Seller',
|
||||
},
|
||||
{
|
||||
desc: 'Your product, Our responsibility.For a measly fee, we will handle the delivery process for you.',
|
||||
title: 'Fulfilled by Company name',
|
||||
value: 'Fulfilled by Company name',
|
||||
},
|
||||
]
|
||||
|
||||
const shippingType = ref('Fulfilled by Company name')
|
||||
const deliveryType = ref('Worldwide delivery')
|
||||
|
||||
const selectedAttrs = ref([
|
||||
'Biodegradable',
|
||||
'Expiry Date',
|
||||
])
|
||||
|
||||
const inventoryTabsData = [
|
||||
{
|
||||
icon: 'ri-add-line',
|
||||
title: 'Restock',
|
||||
value: 'Restock',
|
||||
},
|
||||
{
|
||||
icon: 'ri-flight-takeoff-line',
|
||||
title: 'Shipping',
|
||||
value: 'Shipping',
|
||||
},
|
||||
{
|
||||
icon: 'ri-map-pin-line',
|
||||
title: 'Global Delivery',
|
||||
value: 'Global Delivery',
|
||||
},
|
||||
{
|
||||
icon: 'ri-attachment-2',
|
||||
title: 'Attributes',
|
||||
value: 'Attributes',
|
||||
},
|
||||
{
|
||||
icon: 'ri-lock-unlock-line',
|
||||
title: 'Advanced',
|
||||
value: 'Advanced',
|
||||
},
|
||||
]
|
||||
|
||||
const inStock = ref(true)
|
||||
const isTaxable = ref(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex flex-wrap justify-space-between gap-4 mb-6">
|
||||
<div class="d-flex flex-column justify-center">
|
||||
<h4 class="text-h4">
|
||||
Add a new product
|
||||
</h4>
|
||||
<span class="text-medium-emphasis">Orders placed across your store</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-4 align-center flex-wrap">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Discard
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
>
|
||||
Save Draft
|
||||
</VBtn>
|
||||
<VBtn>Publish Product</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VRow>
|
||||
<VCol md="8">
|
||||
<!-- 👉 Product Information -->
|
||||
<VCard
|
||||
class="mb-6"
|
||||
title="Product Information"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
label="Product Name"
|
||||
placeholder="iPhone 14"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="SKU"
|
||||
placeholder="FXSK123U"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
label="Barcode"
|
||||
placeholder="0123-4567"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VLabel class="mb-1">
|
||||
Description (Optional)
|
||||
</VLabel>
|
||||
<TiptapEditor
|
||||
v-model="content"
|
||||
class="border rounded"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Product Image -->
|
||||
<VCard class="mb-6">
|
||||
<VCardItem>
|
||||
<template #title>
|
||||
Product Image
|
||||
</template>
|
||||
<template #append>
|
||||
<div class="text-primary font-weight-medium cursor-pointer">
|
||||
Add Media from URL
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<div class="flex">
|
||||
<div class="w-full h-auto relative">
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
class="cursor-pointer"
|
||||
@click="() => open()"
|
||||
>
|
||||
<div
|
||||
v-if="fileData.length === 0"
|
||||
class="d-flex flex-column justify-center align-center gap-y-2 pa-12 border-dashed drop-zone"
|
||||
>
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
rounded
|
||||
>
|
||||
<VIcon icon="ri-upload-2-line" />
|
||||
</VAvatar>
|
||||
<h4 class="text-h4">
|
||||
Drag and Drop Your Image Here.
|
||||
</h4>
|
||||
<span class="text-disabled">or</span>
|
||||
|
||||
<VBtn variant="outlined">
|
||||
Browse Images
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="d-flex justify-center align-center gap-3 pa-8 border-dashed drop-zone flex-wrap"
|
||||
>
|
||||
<VRow class="match-height w-100">
|
||||
<template
|
||||
v-for="(item, index) in fileData"
|
||||
:key="index"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard
|
||||
:ripple="false"
|
||||
border
|
||||
>
|
||||
<VCardText
|
||||
class="d-flex flex-column"
|
||||
@click.stop
|
||||
>
|
||||
<VImg
|
||||
:src="item.url"
|
||||
width="200px"
|
||||
height="150px"
|
||||
class="w-100 mx-auto"
|
||||
/>
|
||||
<div class="mt-2">
|
||||
<span class="clamp-text text-wrap">
|
||||
{{ item.file.name }}
|
||||
</span>
|
||||
<span>
|
||||
{{ item.file.size / 1000 }} KB
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
variant="text"
|
||||
block
|
||||
@click.stop="fileData.splice(index, 1)"
|
||||
>
|
||||
Remove File
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Variants -->
|
||||
<VCard
|
||||
title="Variants"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<template
|
||||
v-for="i in optionCounter"
|
||||
:key="i"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
:items="['Size', 'Color', 'Weight', 'Smell']"
|
||||
placeholder="Select Variant"
|
||||
label="Select Variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
label="Variant Value"
|
||||
type="number"
|
||||
placeholder="Enter Variant Value"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<VBtn
|
||||
class="mt-6"
|
||||
@click="optionCounter++"
|
||||
>
|
||||
Add another option
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Inventory -->
|
||||
<VCard
|
||||
title="Inventory"
|
||||
class="inventory-card"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
direction="vertical"
|
||||
color="primary"
|
||||
class="v-tabs-pill"
|
||||
>
|
||||
<VTab
|
||||
v-for="(tab, index) in inventoryTabsData"
|
||||
:key="index"
|
||||
:value="tab.value"
|
||||
>
|
||||
<VIcon
|
||||
:icon="tab.icon"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ tab.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
</VCol>
|
||||
|
||||
<VDivider
|
||||
:vertical="$vuetify.display.mdAndUp ? true : false"
|
||||
inset
|
||||
/>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VWindow
|
||||
v-model="activeTab"
|
||||
class="w-100"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem value="Restock">
|
||||
<div class="d-flex flex-column gap-y-4">
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
Options
|
||||
</div>
|
||||
<div class="d-flex gap-x-4 align-center">
|
||||
<VTextField
|
||||
label="Add to stock"
|
||||
placeholder="100"
|
||||
density="compact"
|
||||
/>
|
||||
<VBtn prepend-icon="ri-check-line">
|
||||
Confirm
|
||||
</VBtn>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-2 text-high-emphasis">
|
||||
<div>
|
||||
Product in stock now: 54
|
||||
</div>
|
||||
<div>
|
||||
Product in transit: 390
|
||||
</div>
|
||||
<div>
|
||||
Last time restocked: 24th June, 2022
|
||||
</div>
|
||||
<div>
|
||||
Total stock over lifetime: 2,430
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Shipping">
|
||||
<VRadioGroup v-model="shippingType">
|
||||
<template #label>
|
||||
<span class="font-weight-medium mb-1">Shipping Type</span>
|
||||
</template>
|
||||
<VRadio
|
||||
v-for="item in shippingList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="mb-4 ps-1"
|
||||
inline
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
<div class="text-high-emphasis font-weight-medium mb-1">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VRadio>
|
||||
</VRadioGroup>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Global Delivery">
|
||||
<div>
|
||||
<VRadioGroup v-model="deliveryType">
|
||||
<template #label>
|
||||
<span class="font-weight-medium mb-1">Global Delivery</span>
|
||||
</template>
|
||||
|
||||
<VRadio
|
||||
value="Worldwide delivery"
|
||||
class="mb-4 ps-1"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
<div class="text-high-emphasis font-weight-medium mb-1">
|
||||
Worldwide delivery
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
Only available with Shipping method:
|
||||
<span class="text-primary">
|
||||
Fulfilled by Company name
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VRadio>
|
||||
|
||||
<VRadio
|
||||
value="Selected Countries"
|
||||
class="mb-4 ps-1"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
<div class="text-high-emphasis font-weight-medium mb-1">
|
||||
Selected Countries
|
||||
</div>
|
||||
<VTextField
|
||||
placeholder="USA"
|
||||
style="min-inline-size: 200px;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VRadio>
|
||||
|
||||
<VRadio>
|
||||
<template #label>
|
||||
<div>
|
||||
<div class="text-high-emphasis font-weight-medium mb-1">
|
||||
Local delivery
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
Deliver to your country of residence
|
||||
<span class="text-primary">
|
||||
Change profile address
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VRadio>
|
||||
</VRadioGroup>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Attributes">
|
||||
<div class="mb-2 text-base font-weight-medium">
|
||||
Attributes
|
||||
</div>
|
||||
<div>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
label="Fragile Product"
|
||||
value="Fragile Product"
|
||||
class="ps-3"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Biodegradable"
|
||||
label="Biodegradable"
|
||||
class="ps-3"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Frozen Product"
|
||||
class="ps-3"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div>Frozen Product</div>
|
||||
<VTextField
|
||||
placeholder="40 C"
|
||||
type="number"
|
||||
style="min-inline-size: 250px;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VCheckbox>
|
||||
|
||||
<VCheckbox
|
||||
v-model="selectedAttrs"
|
||||
value="Expiry Date"
|
||||
class="ps-3"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div>Expiry Date of Product</div>
|
||||
<AppDateTimePicker
|
||||
model-value="2025-06-14"
|
||||
placeholder="Select a Date"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VCheckbox>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="Advanced">
|
||||
<div class="mb-3 text-base font-weight-medium">
|
||||
Advanced
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="7"
|
||||
>
|
||||
<VSelect
|
||||
style="min-inline-size: 200px;"
|
||||
label="Product ID Type"
|
||||
placeholder="Select Product Type"
|
||||
:items="['ISBN', 'UPC', 'EAN', 'JAN']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="5"
|
||||
>
|
||||
<VTextField
|
||||
label="Product Id"
|
||||
placeholder="100023"
|
||||
type="number"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
md="4"
|
||||
cols="12"
|
||||
>
|
||||
<!-- 👉 Pricing -->
|
||||
<VCard
|
||||
title="Pricing"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VTextField
|
||||
label="Base Price"
|
||||
placeholder="iPhone 14"
|
||||
class="mb-6"
|
||||
/>
|
||||
<VTextField
|
||||
label="Discounted Price"
|
||||
placeholder="$499"
|
||||
class="mb-4"
|
||||
/>
|
||||
<VCheckbox
|
||||
v-model="isTaxable"
|
||||
label="Charge Tax on this product"
|
||||
/>
|
||||
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<div class="d-flex flex-raw align-center justify-space-between ">
|
||||
<span>In stock</span>
|
||||
<VSwitch
|
||||
v-model="inStock"
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Organize -->
|
||||
<VCard title="Organize">
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column gap-y-4">
|
||||
<VSelect
|
||||
placeholder="Select Vendor"
|
||||
label="Vendor"
|
||||
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
|
||||
/>
|
||||
<VSelect
|
||||
placeholder="Select Category"
|
||||
label="Category"
|
||||
:items="['Household', 'Office', 'Electronics', 'Management', 'Automotive']"
|
||||
>
|
||||
<template #append>
|
||||
<IconBtn
|
||||
icon="ri-add-line"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
</VSelect>
|
||||
<VSelect
|
||||
placeholder="Select Collection"
|
||||
label="Collection"
|
||||
:items="['Men\'s Clothing', 'Women\'s Clothing', 'Kid\'s Clothing']"
|
||||
/>
|
||||
<VSelect
|
||||
placeholder="Select Status"
|
||||
label="Status"
|
||||
:items="['Published', 'Inactive', 'Scheduled']"
|
||||
/>
|
||||
<VTextField
|
||||
label="Tags"
|
||||
placeholder="Fashion, Trending, Summer"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drop-zone {
|
||||
border: 1px dashed rgba(var(--v-theme-on-surface), 0.12);
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.inventory-card{
|
||||
.v-radio-group,
|
||||
.v-checkbox {
|
||||
.v-selection-control {
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
.v-label.custom-input {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror{
|
||||
p{
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
padding: 0.5rem;
|
||||
outline: none;
|
||||
|
||||
p.is-editor-empty:first-child::before {
|
||||
block-size: 0;
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: inline-start;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
310
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal file
310
resources/js/pages/apps/ecommerce/product/category-list.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<script setup>
|
||||
import ECommerceAddCategoryDrawer from '@/views/apps/ecommerce/EcommerceAddCategoryDrawer.vue'
|
||||
import product1 from '@images/ecommerce-images/product-1.png'
|
||||
import product10 from '@images/ecommerce-images/product-10.png'
|
||||
import product11 from '@images/ecommerce-images/product-11.png'
|
||||
import product12 from '@images/ecommerce-images/product-12.png'
|
||||
import product14 from '@images/ecommerce-images/product-14.png'
|
||||
import product17 from '@images/ecommerce-images/product-17.png'
|
||||
import product19 from '@images/ecommerce-images/product-19.png'
|
||||
import product2 from '@images/ecommerce-images/product-2.png'
|
||||
import product25 from '@images/ecommerce-images/product-25.png'
|
||||
import product28 from '@images/ecommerce-images/product-28.png'
|
||||
import product9 from '@images/ecommerce-images/product-9.png'
|
||||
|
||||
const categoryData = [
|
||||
{
|
||||
categoryTitle: 'Smart Phone',
|
||||
description: 'Choose from wide range of smartphones online at best prices.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product1,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Clothing, Shoes, and jewellery',
|
||||
description: 'Fashion for a wide selection of clothing, shoes, jewellery and watches.',
|
||||
totalProduct: 4689,
|
||||
totalEarning: 45627,
|
||||
image: product9,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Home and Kitchen',
|
||||
description: 'Browse through the wide range of Home and kitchen products.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product10,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Beauty and Personal Care',
|
||||
description: 'Explore beauty and personal care products, shop makeup and etc.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product19,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Books',
|
||||
description: 'Over 25 million titles across categories such as business and etc.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product25,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Games',
|
||||
description: 'Every month, get exclusive in-game loot, free games, a free subscription.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product12,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Baby Products',
|
||||
description: 'Buy baby products across different categories from top brands.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product14,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Growsari',
|
||||
description: 'Shop grocery Items through at best prices in India.',
|
||||
totalProduct: 12548,
|
||||
totalEarning: 98784,
|
||||
image: product28,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Computer Accessories',
|
||||
description: 'Enhance your computing experience with our range of computer accessories.',
|
||||
totalProduct: 9876,
|
||||
totalEarning: 65421,
|
||||
image: product17,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Fitness Tracker',
|
||||
description: 'Monitor your health and fitness goals with our range of advanced fitness trackers.',
|
||||
totalProduct: 1987,
|
||||
totalEarning: 32067,
|
||||
image: product10,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Smart Home Devices',
|
||||
description: 'Transform your home into a smart home with our innovative smart home devices.',
|
||||
totalProduct: 2345,
|
||||
totalEarning: 87654,
|
||||
image: product11,
|
||||
},
|
||||
{
|
||||
categoryTitle: 'Audio Speakers',
|
||||
description: 'Immerse yourself in rich audio quality with our wide range of speakers.',
|
||||
totalProduct: 5678,
|
||||
totalEarning: 32145,
|
||||
image: product2,
|
||||
},
|
||||
]
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Categories',
|
||||
key: 'categoryTitle',
|
||||
},
|
||||
{
|
||||
title: 'Total Products',
|
||||
key: 'totalProduct',
|
||||
align: 'end',
|
||||
},
|
||||
{
|
||||
title: 'Total Earning',
|
||||
key: 'totalEarning',
|
||||
align: 'end',
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const itemsPerPage = ref(10)
|
||||
const searchQuery = ref('')
|
||||
const isAddProductDrawerOpen = ref(false)
|
||||
const page = ref(1)
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-md-space-between flex-wrap gap-4 justify-center">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search"
|
||||
density="compact"
|
||||
style="max-inline-size: 280px; min-inline-size: 200px;"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<VBtn
|
||||
prepend-icon="ri-upload-2-line"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
<VBtn
|
||||
prepend-icon="ri-add-line"
|
||||
@click="isAddProductDrawerOpen = !isAddProductDrawerOpen"
|
||||
>
|
||||
Add Category
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTable
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:headers="headers"
|
||||
:page="page"
|
||||
:items="categoryData"
|
||||
item-value="categoryTitle"
|
||||
:search="searchQuery"
|
||||
show-select
|
||||
class="text-no-wrap category-table"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.actions>
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-edit-box-line" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn
|
||||
size="small"
|
||||
:menu-list="[
|
||||
{
|
||||
title: 'Download',
|
||||
value: 'download',
|
||||
prependIcon: 'ri-download-line',
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
value: 'edit',
|
||||
prependIcon: 'ri-pencil-line',
|
||||
},
|
||||
{
|
||||
title: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
prependIcon: 'ri-stack-line',
|
||||
},
|
||||
]"
|
||||
item-props
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item.categoryTitle="{ item }">
|
||||
<div class="d-flex gap-x-3">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="38"
|
||||
>
|
||||
<img
|
||||
:src="item.image"
|
||||
:alt="item.categoryTitle"
|
||||
width="38"
|
||||
height="38"
|
||||
>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<p class="text-high-emphasis font-weight-medium mb-0">
|
||||
{{ item.categoryTitle }}
|
||||
</p>
|
||||
<div class="text-body-2">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.totalEarning="{ item }">
|
||||
<div class="text-end pe-4">
|
||||
{{ (item.totalEarning).toLocaleString("en-IN", { style: "currency", currency: 'USD' }) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.totalProduct="{ item }">
|
||||
<div class="text-end pe-4">
|
||||
{{ (item.totalProduct).toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, categoryData.length) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(categoryData.length / itemsPerPage)"
|
||||
@click="page >= Math.ceil(categoryData.length / itemsPerPage) ? page = Math.ceil(categoryData.length / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<ECommerceAddCategoryDrawer v-model:isDrawerOpen="isAddProductDrawerOpen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ProseMirror-focused{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.category-table.v-table.v-data-table{
|
||||
.v-table__wrapper{
|
||||
table{
|
||||
thead{
|
||||
tr{
|
||||
th.v-data-table-column--align-end{
|
||||
.v-data-table-header__content{
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
518
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal file
518
resources/js/pages/apps/ecommerce/product/list/index.vue
Normal file
@@ -0,0 +1,518 @@
|
||||
<script setup>
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'In-Store Sales',
|
||||
value: '$5,345',
|
||||
icon: 'ri-home-6-line',
|
||||
desc: '5k orders',
|
||||
change: 5.7,
|
||||
},
|
||||
{
|
||||
title: 'Website Sales',
|
||||
value: '$74,347',
|
||||
icon: 'ri-computer-line',
|
||||
desc: '21k orders',
|
||||
change: 12.4,
|
||||
},
|
||||
{
|
||||
title: 'Discount',
|
||||
value: '$14,235',
|
||||
icon: 'ri-gift-line',
|
||||
desc: '6k orders',
|
||||
},
|
||||
{
|
||||
title: 'Affiliate',
|
||||
value: '$8,345',
|
||||
icon: 'ri-money-dollar-circle-line',
|
||||
desc: '150 orders',
|
||||
change: -3.5,
|
||||
},
|
||||
])
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: 'Product',
|
||||
key: 'product',
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
key: 'category',
|
||||
},
|
||||
{
|
||||
title: 'Stock',
|
||||
key: 'stock',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
key: 'sku',
|
||||
},
|
||||
{
|
||||
title: 'Price',
|
||||
key: 'price',
|
||||
},
|
||||
{
|
||||
title: 'QTY',
|
||||
key: 'qty',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const selectedStatus = ref()
|
||||
const selectedCategory = ref()
|
||||
const selectedStock = ref()
|
||||
const searchQuery = ref('')
|
||||
|
||||
const status = ref([
|
||||
{
|
||||
title: 'Scheduled',
|
||||
value: 'Scheduled',
|
||||
},
|
||||
{
|
||||
title: 'Publish',
|
||||
value: 'Published',
|
||||
},
|
||||
{
|
||||
title: 'Inactive',
|
||||
value: 'Inactive',
|
||||
},
|
||||
])
|
||||
|
||||
const categories = ref([
|
||||
{
|
||||
title: 'Accessories',
|
||||
value: 'Accessories',
|
||||
},
|
||||
{
|
||||
title: 'Home Decor',
|
||||
value: 'Home Decor',
|
||||
},
|
||||
{
|
||||
title: 'Electronics',
|
||||
value: 'Electronics',
|
||||
},
|
||||
{
|
||||
title: 'Shoes',
|
||||
value: 'Shoes',
|
||||
},
|
||||
{
|
||||
title: 'Office',
|
||||
value: 'Office',
|
||||
},
|
||||
{
|
||||
title: 'Games',
|
||||
value: 'Games',
|
||||
},
|
||||
])
|
||||
|
||||
const stockStatus = ref([
|
||||
{
|
||||
title: 'In Stock',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
title: 'Out of Stock',
|
||||
value: false,
|
||||
},
|
||||
])
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const resolveCategory = category => {
|
||||
if (category === 'Accessories')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'ri-headphone-line',
|
||||
}
|
||||
if (category === 'Home Decor')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'ri-home-6-line',
|
||||
}
|
||||
if (category === 'Electronics')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-computer-line',
|
||||
}
|
||||
if (category === 'Shoes')
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'ri-footprint-line',
|
||||
}
|
||||
if (category === 'Office')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'ri-briefcase-line',
|
||||
}
|
||||
if (category === 'Games')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-gamepad-line',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStatus = statusMsg => {
|
||||
if (statusMsg === 'Scheduled')
|
||||
return {
|
||||
text: 'Scheduled',
|
||||
color: 'warning',
|
||||
}
|
||||
if (statusMsg === 'Published')
|
||||
return {
|
||||
text: 'Publish',
|
||||
color: 'success',
|
||||
}
|
||||
if (statusMsg === 'Inactive')
|
||||
return {
|
||||
text: 'Inactive',
|
||||
color: 'error',
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data: productsData,
|
||||
execute: fetchProducts,
|
||||
} = await useApi(createUrl('/apps/ecommerce/products', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
stock: selectedStock,
|
||||
category: selectedCategory,
|
||||
status: selectedStatus,
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const products = computed(() => productsData.value.products)
|
||||
const totalProduct = computed(() => productsData.value.total)
|
||||
|
||||
const deleteProduct = async id => {
|
||||
await $api(`apps/ecommerce/products/${ id }`, { method: 'DELETE' })
|
||||
fetchProducts()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 👉 widgets -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-2">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, index) in widgetData"
|
||||
:key="index"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-space-between"
|
||||
:class="$vuetify.display.xs ? 'product-widget' : $vuetify.display.sm ? index < 2 ? 'product-widget' : '' : ''"
|
||||
>
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<p class="text-capitalize mb-0">
|
||||
{{ data.title }}
|
||||
</p>
|
||||
|
||||
<h6 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h6>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<div class="text-no-wrap me-2">
|
||||
{{ data.desc }}
|
||||
</div>
|
||||
|
||||
<VChip
|
||||
v-if="data.change"
|
||||
size="small"
|
||||
:color="data.change > 0 ? 'success' : 'error'"
|
||||
>
|
||||
{{ prefixWithPlus(data.change) }}%
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="44"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="28"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? index !== widgetData.length - 1 : $vuetify.display.smAndUp ? index % 2 === 0 : false"
|
||||
vertical
|
||||
inset
|
||||
length="100"
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 products -->
|
||||
<VCard title="Filters">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 👉 Select Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedStatus"
|
||||
label="Select Status"
|
||||
placeholder="Select Status"
|
||||
:items="status"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Category -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedCategory"
|
||||
label="Category"
|
||||
placeholder="Select Category"
|
||||
:items="categories"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Stock Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedStock"
|
||||
label="Stock"
|
||||
placeholder="Stock"
|
||||
:items="stockStatus"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<div class="d-flex align-center">
|
||||
<!-- 👉 Search -->
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search Product"
|
||||
style="inline-size: 200px;"
|
||||
density="compact"
|
||||
class="me-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="d-flex gap-x-4">
|
||||
<!-- 👉 Export button -->
|
||||
<div>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prepend-icon="ri-external-link-line"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
prepend-icon="ri-add-line"
|
||||
@click="$router.push('/apps/ecommerce/product/add')"
|
||||
>
|
||||
Add Product
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:headers="headers"
|
||||
show-select
|
||||
:items="products"
|
||||
:items-length="totalProduct"
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- product -->
|
||||
<template #item.product="{ item }">
|
||||
<div class="d-flex align-center gap-x-4">
|
||||
<VAvatar
|
||||
v-if="item.image"
|
||||
size="38"
|
||||
variant="tonal"
|
||||
rounded
|
||||
:image="item.image"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-base text-high-emphasis font-weight-medium">{{ item.productName }}</span>
|
||||
<span class="text-sm">{{ item.productBrand }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- category -->
|
||||
<template #item.category="{ item }">
|
||||
<VAvatar
|
||||
size="30"
|
||||
variant="tonal"
|
||||
:color="resolveCategory(item.category)?.color"
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon
|
||||
:icon="resolveCategory(item.category)?.icon"
|
||||
size="18"
|
||||
/>
|
||||
</VAvatar>
|
||||
<span class="text-base text-high-emphasis">{{ item.category }}</span>
|
||||
</template>
|
||||
|
||||
<!-- stock -->
|
||||
<template #item.stock="{ item }">
|
||||
<VSwitch :model-value="item.stock" />
|
||||
</template>
|
||||
|
||||
<!-- status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-edit-box-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn size="small">
|
||||
<VIcon icon="ri-more-2-fill" />
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem
|
||||
value="download"
|
||||
prepend-icon="ri-download-line"
|
||||
>
|
||||
Download
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="delete"
|
||||
prepend-icon="ri-delete-bin-line"
|
||||
@click="deleteProduct(item.id)"
|
||||
>
|
||||
Delete
|
||||
</VListItem>
|
||||
|
||||
<VListItem
|
||||
value="duplicate"
|
||||
prepend-icon="ri-stack-line"
|
||||
>
|
||||
Duplicate
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalProduct) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalProduct / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalProduct / itemsPerPage) ? page = Math.ceil(totalProduct / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-widget{
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
384
resources/js/pages/apps/ecommerce/referrals.vue
Normal file
384
resources/js/pages/apps/ecommerce/referrals.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<script setup>
|
||||
import paperImg from '@images/svg/paper.svg?raw'
|
||||
import rocketImg from '@images/svg/rocket.svg?raw'
|
||||
import userInfoImg from '@images/svg/user-info.svg?raw'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
|
||||
const rocketIcon = h('div', {
|
||||
innerHTML: rocketImg,
|
||||
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
|
||||
})
|
||||
|
||||
const userInfoIcon = h('div', {
|
||||
innerHTML: paperImg,
|
||||
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
|
||||
})
|
||||
|
||||
const paperIcon = h('div', {
|
||||
innerHTML: userInfoImg,
|
||||
style: 'font-size: 2.625rem; color: rgb(var(--v-theme-primary))',
|
||||
})
|
||||
|
||||
const widgetData = [
|
||||
{
|
||||
title: 'Total Earning',
|
||||
value: '$24,983',
|
||||
icon: 'ri-money-dollar-circle-line',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Unpaid Earning',
|
||||
value: '$8,647',
|
||||
icon: 'ri-gift-line',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Signups',
|
||||
value: '2,367',
|
||||
icon: 'ri-group-line',
|
||||
color: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Conversion Rate',
|
||||
value: '4.5%',
|
||||
icon: 'ri-refresh-line',
|
||||
color: 'info',
|
||||
},
|
||||
]
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
icon: rocketIcon,
|
||||
desc: 'Create & validate your referral link and get',
|
||||
value: '$50',
|
||||
},
|
||||
{
|
||||
icon: paperIcon,
|
||||
desc: 'For every new signup you get',
|
||||
value: '10%',
|
||||
},
|
||||
{
|
||||
icon: userInfoIcon,
|
||||
desc: 'Get other friends to generate link and get',
|
||||
value: '$100',
|
||||
},
|
||||
]
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
// Data Table Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'Users',
|
||||
key: 'users',
|
||||
},
|
||||
{
|
||||
title: 'Referred ID',
|
||||
key: 'referred-id',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
key: 'value',
|
||||
},
|
||||
{
|
||||
title: 'Earnings',
|
||||
key: 'earning',
|
||||
},
|
||||
]
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const { data: referralData } = await useApi(createUrl('/apps/ecommerce/referrals', {
|
||||
query: {
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const referrals = computed(() => referralData.value.referrals)
|
||||
const totalReferrals = computed(() => referralData.value.total)
|
||||
|
||||
const resolveStatus = status => {
|
||||
if (status === 'Rejected')
|
||||
return {
|
||||
text: 'Rejected',
|
||||
color: 'error',
|
||||
}
|
||||
if (status === 'Unpaid')
|
||||
return {
|
||||
text: 'Unpaid',
|
||||
color: 'warning',
|
||||
}
|
||||
if (status === 'Paid')
|
||||
return {
|
||||
text: 'Paid',
|
||||
color: 'success',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 👉 Header -->
|
||||
<VRow class="match-height">
|
||||
<!-- 👉 Widgets -->
|
||||
<VCol
|
||||
v-for="(data, index) in widgetData"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="3"
|
||||
sm="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-h5 mb-1">{{ data.value }}</span>
|
||||
<span class="text-sm">{{ data.title }}</span>
|
||||
</div>
|
||||
<VAvatar
|
||||
:icon="data.icon"
|
||||
variant="tonal"
|
||||
:color="data.color"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Icon Steps -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle> How to use</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
Integrate your referral code in 3 easy steps.
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<div class="d-flex gap-6 justify-space-between align-center flex-sm-row flex-column">
|
||||
<div
|
||||
v-for="(step, index) in stepsData"
|
||||
:key="index"
|
||||
class="d-flex flex-column align-center gap-y-2"
|
||||
style="max-inline-size: 185px;"
|
||||
>
|
||||
<div class="icon-container">
|
||||
<VNodeRenderer :nodes="step.icon" />
|
||||
</div>
|
||||
<span class="text-body-1 text-wrap text-center">
|
||||
{{ step.desc }}
|
||||
</span>
|
||||
<span class="text-primary text-h6">{{ step.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Invite -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="mb-11">
|
||||
<h5 class="text-h5 mb-5">
|
||||
Invite your friends
|
||||
</h5>
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<VTextField
|
||||
placeholder="Email Addresss"
|
||||
density="compact"
|
||||
/>
|
||||
<VBtn>
|
||||
<VIcon
|
||||
start
|
||||
icon="ri-check-line"
|
||||
/>
|
||||
Submit
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-h5 mb-5">
|
||||
Share the referral link
|
||||
</h5>
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<VTextField
|
||||
placeholder="themeselection.com/?ref=6478"
|
||||
density="compact"
|
||||
/>
|
||||
<div class="d-flex gap-x-2">
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#3B5998"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="ri-facebook-circle-line"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#55ACEE"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="ri-twitter-line"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Referral Table -->
|
||||
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap gap-4">
|
||||
<h5 class="text-h5">
|
||||
Referred Users
|
||||
</h5>
|
||||
<VBtn prepend-icon="ri-upload-2-line">
|
||||
Export
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:items="referrals"
|
||||
:headers="headers"
|
||||
:items-length="totalReferrals"
|
||||
show-select
|
||||
class="text-high-emphasis"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #item.users="{ item }">
|
||||
<div class="d-flex align-center gap-x-4">
|
||||
<VAvatar
|
||||
:image="item.avatar"
|
||||
:size="34"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
<RouterLink
|
||||
class="text-high-emphasis"
|
||||
:to="{ name: 'apps-ecommerce-customer-details-id', params: { id: 478426 } }"
|
||||
>
|
||||
{{ item.user }}
|
||||
</RouterLink>
|
||||
</h6>
|
||||
|
||||
<div class="text-sm text-medium-emphasis">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.referred-id="{ item }">
|
||||
{{ item.referredId }}
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-bind="resolveStatus(item.status)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalReferrals) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalReferrals / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalReferrals / itemsPerPage) ? page = Math.ceil(totalReferrals / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed rgb(var(--v-theme-primary));
|
||||
border-radius: 50%;
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #000;
|
||||
font-size: 42px;
|
||||
}
|
||||
</style>
|
99
resources/js/pages/apps/ecommerce/settings.vue
Normal file
99
resources/js/pages/apps/ecommerce/settings.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import SettingsCheckout from '@/views/apps/ecommerce/settings/SettingsCheckout.vue'
|
||||
import SettingsLocations from '@/views/apps/ecommerce/settings/SettingsLocations.vue'
|
||||
import SettingsNotifications from '@/views/apps/ecommerce/settings/SettingsNotifications.vue'
|
||||
import SettingsPayment from '@/views/apps/ecommerce/settings/SettingsPayment.vue'
|
||||
import SettingsShippingAndDelivery from '@/views/apps/ecommerce/settings/SettingsShippingAndDelivery.vue'
|
||||
import SettingsStoreDetails from '@/views/apps/ecommerce/settings/SettingsStoreDetails.vue'
|
||||
|
||||
const tabsData = [
|
||||
{
|
||||
icon: 'ri-store-line',
|
||||
title: 'Store Details',
|
||||
},
|
||||
{
|
||||
icon: 'ri-bank-card-line',
|
||||
title: 'Payments',
|
||||
},
|
||||
{
|
||||
icon: 'ri-shopping-cart-line',
|
||||
title: 'Checkout',
|
||||
},
|
||||
{
|
||||
icon: 'ri-car-line',
|
||||
title: 'Shipping & Delivery',
|
||||
},
|
||||
{
|
||||
icon: 'ri-map-pin-line',
|
||||
title: 'Location',
|
||||
},
|
||||
{
|
||||
icon: 'ri-notification-3-line',
|
||||
title: 'Notifications',
|
||||
},
|
||||
]
|
||||
|
||||
const activeTab = ref(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<h5 class="text-h5 mb-4">
|
||||
Getting Started
|
||||
</h5>
|
||||
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
direction="vertical"
|
||||
class="v-tabs-pill disable-tab-transition"
|
||||
>
|
||||
<VTab
|
||||
v-for="(tabItem, index) in tabsData"
|
||||
:key="index"
|
||||
:prepend-icon="tabItem.icon"
|
||||
>
|
||||
{{ tabItem.title }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VWindow
|
||||
v-model="activeTab"
|
||||
class="disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem>
|
||||
<SettingsStoreDetails />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<SettingsPayment />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<SettingsCheckout />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<SettingsShippingAndDelivery />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<SettingsLocations />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<SettingsNotifications />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
511
resources/js/pages/apps/email/index.vue
Normal file
511
resources/js/pages/apps/email/index.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import ComposeDialog from '@/views/apps/email/ComposeDialog.vue'
|
||||
import EmailLeftSidebarContent from '@/views/apps/email/EmailLeftSidebarContent.vue'
|
||||
import EmailView from '@/views/apps/email/EmailView.vue'
|
||||
import { useEmail } from '@/views/apps/email/useEmail'
|
||||
|
||||
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
|
||||
|
||||
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
|
||||
|
||||
// Composables
|
||||
const route = useRoute()
|
||||
const { labels, resolveLabelColor, emailMoveToFolderActions, shallShowMoveToActionFor, moveSelectedEmailTo, updateEmails, updateEmailLabels } = useEmail()
|
||||
|
||||
// Compose dialog
|
||||
const isComposeDialogVisible = ref(false)
|
||||
|
||||
// Ref
|
||||
const q = ref('')
|
||||
|
||||
// Email Selection
|
||||
|
||||
// ------------------------------------------------
|
||||
const selectedEmails = ref([])
|
||||
|
||||
const {
|
||||
data: emailData,
|
||||
execute: fetchEmails,
|
||||
} = await useApi(createUrl('/apps/email', {
|
||||
query: {
|
||||
q,
|
||||
filter: () => 'filter' in route.params ? route.params.filter : undefined,
|
||||
label: () => 'label' in route.params ? route.params.label : undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
const emails = computed(() => emailData.value.emails)
|
||||
|
||||
const toggleSelectedEmail = emailId => {
|
||||
const emailIndex = selectedEmails.value.indexOf(emailId)
|
||||
if (emailIndex === -1)
|
||||
selectedEmails.value.push(emailId)
|
||||
else
|
||||
selectedEmails.value.splice(emailIndex, 1)
|
||||
}
|
||||
|
||||
const selectAllEmailCheckbox = computed(() => emails.value.length && emails.value.length === selectedEmails.value.length)
|
||||
const isSelectAllEmailCheckboxIndeterminate = computed(() => Boolean(selectedEmails.value.length) && emails.value.length !== selectedEmails.value.length)
|
||||
|
||||
const isAllMarkRead = computed(() => {
|
||||
return selectedEmails.value.every(emailId => emails.value.find(email => email.id === emailId)?.isRead)
|
||||
})
|
||||
|
||||
const selectAllCheckboxUpdate = () => {
|
||||
selectedEmails.value = !selectAllEmailCheckbox.value ? emails.value.map(email => email.id) : []
|
||||
}
|
||||
|
||||
// Email View
|
||||
const openedEmail = ref(null)
|
||||
|
||||
const emailViewMeta = computed(() => {
|
||||
const returnValue = {
|
||||
hasNextEmail: false,
|
||||
hasPreviousEmail: false,
|
||||
}
|
||||
|
||||
if (openedEmail.value) {
|
||||
const openedEmailIndex = emails.value.findIndex(e => e.id === openedEmail.value?.id)
|
||||
|
||||
returnValue.hasNextEmail = !!emails.value[openedEmailIndex + 1]
|
||||
returnValue.hasPreviousEmail = !!emails.value[openedEmailIndex - 1]
|
||||
}
|
||||
|
||||
return returnValue
|
||||
})
|
||||
|
||||
const refreshOpenedEmail = async () => {
|
||||
await fetchEmails()
|
||||
if (openedEmail.value)
|
||||
openedEmail.value = emails.value.find(e => e.id === openedEmail.value?.id)
|
||||
}
|
||||
|
||||
/*ℹ️ You can optimize it so it doesn't fetch emails on each action.
|
||||
Currently, if you just star the email, two API calls will get fired.
|
||||
1. star the email
|
||||
2. Fetch all latest emails
|
||||
|
||||
You can limit this to single API call by:
|
||||
- making API to star the email
|
||||
- modify the state (set that email's isStarred property to true/false) in the store instead of making API for fetching emails
|
||||
|
||||
😊 For simplicity of the code and possible of modification, we kept it simple.
|
||||
*/
|
||||
const handleActionClick = async (action, emailIds = selectedEmails.value) => {
|
||||
selectedEmails.value = []
|
||||
if (!emailIds.length)
|
||||
return
|
||||
if (action === 'trash')
|
||||
await updateEmails(emailIds, { isDeleted: true })
|
||||
else if (action === 'spam')
|
||||
await updateEmails(emailIds, { folder: 'spam' })
|
||||
else if (action === 'unread')
|
||||
await updateEmails(emailIds, { isRead: false })
|
||||
else if (action === 'read')
|
||||
await updateEmails(emailIds, { isRead: true })
|
||||
else if (action === 'star')
|
||||
await updateEmails(emailIds, { isStarred: true })
|
||||
else if (action === 'unstar')
|
||||
await updateEmails(emailIds, { isStarred: false })
|
||||
await fetchEmails()
|
||||
if (openedEmail.value)
|
||||
refreshOpenedEmail()
|
||||
}
|
||||
|
||||
const handleMoveMailsTo = async action => {
|
||||
moveSelectedEmailTo(action, selectedEmails.value)
|
||||
await fetchEmails()
|
||||
}
|
||||
|
||||
const changeOpenedEmail = dir => {
|
||||
if (!openedEmail.value)
|
||||
return
|
||||
const openedEmailIndex = emails.value.findIndex(e => e.id === openedEmail.value?.id)
|
||||
const newEmailIndex = dir === 'previous' ? openedEmailIndex - 1 : openedEmailIndex + 1
|
||||
|
||||
openedEmail.value = emails.value[newEmailIndex]
|
||||
}
|
||||
|
||||
const openEmail = async email => {
|
||||
openedEmail.value = email
|
||||
await handleActionClick('read', [email.id])
|
||||
}
|
||||
|
||||
watch(() => route.params, () => {
|
||||
selectedEmails.value = []
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLayout
|
||||
style="min-block-size: 100%;"
|
||||
class="email-app-layout"
|
||||
>
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
absolute
|
||||
touchless
|
||||
location="start"
|
||||
:temporary="$vuetify.display.mdAndDown"
|
||||
>
|
||||
<EmailLeftSidebarContent @toggle-compose-dialog-visibility="isComposeDialogVisible = !isComposeDialogVisible" />
|
||||
</VNavigationDrawer>
|
||||
<EmailView
|
||||
:email="openedEmail"
|
||||
:email-meta="emailViewMeta"
|
||||
@refresh="refreshOpenedEmail"
|
||||
@navigated="changeOpenedEmail"
|
||||
@close="openedEmail = null"
|
||||
@remove="handleActionClick('trash', openedEmail ? [openedEmail.id] : [])"
|
||||
@unread="handleActionClick('unread', openedEmail ? [openedEmail.id] : [])"
|
||||
@star="handleActionClick('star', openedEmail ? [openedEmail.id] : [])"
|
||||
@unstar="handleActionClick('unstar', openedEmail ? [openedEmail.id] : [])"
|
||||
/>
|
||||
<VMain>
|
||||
<VCard
|
||||
flat
|
||||
class="email-content-list h-100 d-flex flex-column"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn
|
||||
class="d-lg-none ms-3"
|
||||
@click="isLeftSidebarOpen = true"
|
||||
>
|
||||
<VIcon icon="ri-menu-line" />
|
||||
</IconBtn>
|
||||
<!-- 👉 Search -->
|
||||
<VTextField
|
||||
v-model="q"
|
||||
density="default"
|
||||
class="email-search px-1 flex-grow-1"
|
||||
placeholder="Search mail"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<VIcon
|
||||
color="disabled"
|
||||
size="22"
|
||||
icon="ri-search-line"
|
||||
/>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
<VDivider />
|
||||
<!-- 👉 Action bar -->
|
||||
<div class="py-2 px-4 d-flex align-center">
|
||||
<!-- TODO: Make checkbox primary on indeterminate state -->
|
||||
<VCheckbox
|
||||
:model-value="selectAllEmailCheckbox"
|
||||
:indeterminate="isSelectAllEmailCheckboxIndeterminate"
|
||||
class="me-1"
|
||||
@update:model-value="selectAllCheckboxUpdate"
|
||||
/>
|
||||
<div
|
||||
class="w-100 d-flex align-center action-bar-actions gap-1"
|
||||
:style="{
|
||||
visibility:
|
||||
isSelectAllEmailCheckboxIndeterminate || selectAllEmailCheckbox
|
||||
? undefined
|
||||
: 'hidden',
|
||||
}"
|
||||
>
|
||||
<!-- Trash -->
|
||||
<IconBtn
|
||||
v-show="('filter' in route.params ? route.params.filter !== 'trashed' : true)"
|
||||
@click="handleActionClick('trash')"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Delete Mail
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Mark unread/read -->
|
||||
<IconBtn @click="isAllMarkRead ? handleActionClick('unread') : handleActionClick('read') ">
|
||||
<VIcon :icon="isAllMarkRead ? 'ri-mail-line' : 'ri-mail-open-line'" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ isAllMarkRead ? 'Mark as Unread' : 'Mark as Read' }}
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Move to folder -->
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-folder-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Folder
|
||||
</VTooltip>
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList density="compact">
|
||||
<template
|
||||
v-for="moveTo in emailMoveToFolderActions"
|
||||
:key="moveTo.title"
|
||||
>
|
||||
<VListItem
|
||||
:class="
|
||||
shallShowMoveToActionFor(moveTo.action) ? 'd-flex' : 'd-none'
|
||||
"
|
||||
class="align-center"
|
||||
href="#"
|
||||
@click="handleMoveMailsTo(moveTo.action)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="moveTo.icon"
|
||||
class="me-2"
|
||||
size="20"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="text-capitalize">
|
||||
{{ moveTo.action }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
|
||||
<!-- Update labels -->
|
||||
<IconBtn>
|
||||
<VIcon icon="ri-price-tag-3-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Label
|
||||
</VTooltip>
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="label in labels"
|
||||
:key="label.title"
|
||||
href="#"
|
||||
@click="updateEmailLabels(selectedEmails, label.title)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VBadge
|
||||
inline
|
||||
:color="resolveLabelColor(label.title)"
|
||||
dot
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="ms-2 text-capitalize">
|
||||
{{ label.title }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<IconBtn
|
||||
class="me-1"
|
||||
@click="fetchEmails"
|
||||
>
|
||||
<VIcon icon="ri-refresh-line" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn />
|
||||
</div>
|
||||
<VDivider />
|
||||
<!-- 👉 Emails list -->
|
||||
<PerfectScrollbar
|
||||
tag="ul"
|
||||
:options="{ wheelPropagation: false }"
|
||||
class="email-list"
|
||||
>
|
||||
<li
|
||||
v-for="email in emails"
|
||||
v-show="emails?.length"
|
||||
:key="email.id"
|
||||
class="email-item d-flex align-center pa-4 gap-2 cursor-pointer"
|
||||
:class="[{ 'email-read': email.isRead }]"
|
||||
@click="openEmail(email)"
|
||||
>
|
||||
<VCheckbox
|
||||
:model-value="selectedEmails.includes(email.id)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="toggleSelectedEmail(email.id)"
|
||||
@click.stop
|
||||
/>
|
||||
<IconBtn
|
||||
:color="email.isStarred ? 'warning' : 'default'"
|
||||
@click.stop=" handleActionClick(email.isStarred ? 'unstar' : 'star', [email.id])"
|
||||
>
|
||||
<VIcon icon="ri-star-line" />
|
||||
</IconBtn>
|
||||
<VAvatar size="32">
|
||||
<VImg
|
||||
:src="email.from.avatar"
|
||||
:alt="email.from.name"
|
||||
/>
|
||||
</VAvatar>
|
||||
<h6 class="text-h6">
|
||||
{{ email.from.name }}
|
||||
</h6>
|
||||
<span class="text-body-2 truncate">{{ email.subject }}</span>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- 👉 Email meta -->
|
||||
<div
|
||||
class="email-meta align-center gap-2"
|
||||
:class="$vuetify.display.xs ? 'd-none' : ''"
|
||||
>
|
||||
<VIcon
|
||||
v-for="label in email.labels"
|
||||
:key="label"
|
||||
icon="ri-circle-fill"
|
||||
size="10"
|
||||
:color="resolveLabelColor(label)"
|
||||
/>
|
||||
|
||||
<span class="text-sm text-disabled">
|
||||
{{ formatDateToMonthShort(email.time) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 👉 Email actions -->
|
||||
<div class="email-actions d-none">
|
||||
<IconBtn @click.stop="handleActionClick('trash', [email.id])">
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Delete Mail
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
class="mx-2"
|
||||
@click.stop=" handleActionClick(email.isRead ? 'unread' : 'read', [email.id])"
|
||||
>
|
||||
<VIcon :icon="email.isRead ? 'ri-mail-line' : 'ri-mail-open-line'" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ email.isRead ? 'Mark as Unread' : 'Mark as Read' }}
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
<IconBtn @click.stop="handleActionClick('spam', [email.id])">
|
||||
<VIcon icon="ri-error-warning-line" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
Move to Spam
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-show="!emails.length"
|
||||
class="py-4 px-5 text-center"
|
||||
>
|
||||
<span class="text-high-emphasis">No items found.</span>
|
||||
</li>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
<ComposeDialog
|
||||
v-show="isComposeDialogVisible"
|
||||
@close="isComposeDialogVisible = false"
|
||||
/>
|
||||
</VMain>
|
||||
</VLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/vuetify.scss";
|
||||
@use "@core-scss/base/mixins.scss";
|
||||
|
||||
// ℹ️ Remove border. Using variant plain cause UI issue, caret isn't align in center
|
||||
.email-search {
|
||||
.v-field__outline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.email-app-layout {
|
||||
border-radius: vuetify.$card-border-radius;
|
||||
|
||||
@include mixins.elevation(vuetify.$card-elevation);
|
||||
|
||||
$sel-email-app-layout: &;
|
||||
|
||||
@at-root {
|
||||
.skin--bordered {
|
||||
@include mixins.bordered-skin($sel-email-app-layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-content-list {
|
||||
border-end-start-radius: 0;
|
||||
border-start-start-radius: 0;
|
||||
}
|
||||
|
||||
.email-list {
|
||||
white-space: nowrap;
|
||||
|
||||
.email-item {
|
||||
transition: all 0.2s ease-in-out;
|
||||
will-change: transform, box-shadow;
|
||||
|
||||
&.email-read {
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
||||
}
|
||||
|
||||
& + .email-item {
|
||||
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.email-item .email-meta {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.email-item:hover {
|
||||
transform: translateY(-2px);
|
||||
|
||||
@include mixins.elevation(4);
|
||||
|
||||
// ℹ️ Don't show actions on hover on mobile & tablet devices
|
||||
@media screen and (min-width: 1280px) {
|
||||
.email-actions {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.email-meta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
+ .email-item {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-compose-dialog {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
min-inline-size: 100%;
|
||||
|
||||
@media only screen and (min-width: 800px) {
|
||||
min-inline-size: 712px;
|
||||
}
|
||||
}
|
||||
</style>
|
170
resources/js/pages/apps/invoice/add/index.vue
Normal file
170
resources/js/pages/apps/invoice/add/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import InvoiceEditable from '@/views/apps/invoice/InvoiceEditable.vue'
|
||||
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
|
||||
|
||||
// 👉 Default Blank Data
|
||||
const invoiceData = ref({
|
||||
invoice: {
|
||||
id: 5037,
|
||||
issuedDate: '',
|
||||
service: '',
|
||||
total: 0,
|
||||
avatar: '',
|
||||
invoiceStatus: '',
|
||||
dueDate: '',
|
||||
balance: 0,
|
||||
client: {
|
||||
address: '112, Lorem Ipsum, Florida',
|
||||
company: 'Greeva Inc',
|
||||
companyEmail: 'johndoe@greeva.com',
|
||||
contact: '+1 123 3452 12',
|
||||
country: 'USA',
|
||||
name: 'John Doe',
|
||||
},
|
||||
},
|
||||
paymentDetails: {
|
||||
totalDue: '$12,110.55',
|
||||
bankName: 'American Bank',
|
||||
country: 'United States',
|
||||
iban: 'ETD95476213',
|
||||
swiftCode: 'BR91905',
|
||||
},
|
||||
purchasedProducts: [{
|
||||
title: '',
|
||||
cost: 0,
|
||||
hours: 0,
|
||||
description: '',
|
||||
}],
|
||||
note: '',
|
||||
paymentMethod: '',
|
||||
salesperson: '',
|
||||
thanksNote: '',
|
||||
})
|
||||
|
||||
const paymentTerms = ref(true)
|
||||
const clientNotes = ref(false)
|
||||
const paymentStub = ref(false)
|
||||
const selectedPaymentMethod = ref('Bank Account')
|
||||
|
||||
const paymentMethods = [
|
||||
'Bank Account',
|
||||
'PayPal',
|
||||
'UPI Transfer',
|
||||
]
|
||||
|
||||
const isSendPaymentSidebarVisible = ref(false)
|
||||
|
||||
const addProduct = value => {
|
||||
invoiceData.value?.purchasedProducts.push(value)
|
||||
}
|
||||
|
||||
const removeProduct = id => {
|
||||
invoiceData.value?.purchasedProducts.splice(id, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- 👉 InvoiceEditable -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="9"
|
||||
>
|
||||
<InvoiceEditable
|
||||
:data="invoiceData"
|
||||
@push="addProduct"
|
||||
@remove="removeProduct"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Right Column: Invoice Action -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VCard class="mb-8">
|
||||
<VCardText>
|
||||
<!-- 👉 Send Invoice -->
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="ri-send-plane-line"
|
||||
class="mb-3"
|
||||
@click="isSendPaymentSidebarVisible = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Preview -->
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: '5036' } }"
|
||||
>
|
||||
Preview
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Save -->
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Select payment method -->
|
||||
<VSelect
|
||||
v-model="selectedPaymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Accept Payment Via"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<!-- 👉 Payment Terms -->
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<VLabel for="payment-terms">
|
||||
Payment Terms
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="payment-terms"
|
||||
v-model="paymentTerms"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Client Notes -->
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<VLabel for="client-notes">
|
||||
Client Notes
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="client-notes"
|
||||
v-model="clientNotes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Payment Stub -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<VLabel for="payment-stub">
|
||||
Payment Stub
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="payment-stub"
|
||||
v-model="paymentStub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Send Invoice Sidebar -->
|
||||
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendPaymentSidebarVisible" />
|
||||
</template>
|
166
resources/js/pages/apps/invoice/edit/[id].vue
Normal file
166
resources/js/pages/apps/invoice/edit/[id].vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup>
|
||||
import InvoiceAddPaymentDrawer from '@/views/apps/invoice/InvoiceAddPaymentDrawer.vue'
|
||||
import InvoiceEditable from '@/views/apps/invoice/InvoiceEditable.vue'
|
||||
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
|
||||
|
||||
const invoiceData = ref()
|
||||
const route = useRoute('apps-invoice-edit-id')
|
||||
const { data: invoiceDetails } = await useApi(`/apps/invoice/${ route.params.id }`)
|
||||
|
||||
invoiceData.value = {
|
||||
invoice: invoiceDetails.value.invoice,
|
||||
paymentDetails: invoiceDetails.value.paymentDetails,
|
||||
purchasedProducts: [{
|
||||
title: 'App Design',
|
||||
cost: 24,
|
||||
hours: 2,
|
||||
description: 'Designed UI kit & app pages.',
|
||||
}],
|
||||
note: 'It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance projects. Thank You!',
|
||||
paymentMethod: 'Bank Account',
|
||||
salesperson: 'Tom Cook',
|
||||
thanksNote: 'Thanks for your business',
|
||||
}
|
||||
|
||||
const addProduct = value => {
|
||||
invoiceData.value?.purchasedProducts.push(value)
|
||||
}
|
||||
|
||||
const removeProduct = id => {
|
||||
invoiceData.value?.purchasedProducts.splice(id, 1)
|
||||
}
|
||||
|
||||
const isSendSidebarActive = ref(false)
|
||||
const isAddPaymentSidebarActive = ref(false)
|
||||
const paymentTerms = ref(true)
|
||||
const clientNotes = ref(false)
|
||||
const paymentStub = ref(false)
|
||||
const selectedPaymentMethod = ref('Bank Account')
|
||||
|
||||
const paymentMethods = [
|
||||
'Bank Account',
|
||||
'PayPal',
|
||||
'UPI Transfer',
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<!-- 👉 InvoiceEditable -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="9"
|
||||
>
|
||||
<InvoiceEditable
|
||||
v-if="invoiceData?.invoice"
|
||||
:data="invoiceData"
|
||||
@push="addProduct"
|
||||
@remove="removeProduct"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Right Column: Invoice Action -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VCard class="mb-8">
|
||||
<VCardText>
|
||||
<!-- 👉 Send Invoice Trigger button -->
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="ri-send-plane-line"
|
||||
class="mb-4"
|
||||
@click="isSendSidebarActive = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<!-- 👉 Preview button -->
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="flex-grow-1"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: route.params.id } }"
|
||||
>
|
||||
Preview
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Save button -->
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mb-4 flex-grow-1"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Add Payment trigger button -->
|
||||
<VBtn
|
||||
block
|
||||
color="success"
|
||||
prepend-icon="ri-money-dollar-circle-line"
|
||||
@click="isAddPaymentSidebarActive = true"
|
||||
>
|
||||
Add Payment
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 👉 Accept payment via -->
|
||||
<VSelect
|
||||
v-model="selectedPaymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Accept Payment Via"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<!-- 👉 Payment Terms -->
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<VLabel for="payment-terms">
|
||||
Payment Terms
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="payment-terms"
|
||||
v-model="paymentTerms"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Client Notes -->
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<VLabel for="client-notes">
|
||||
Client Notes
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="client-notes"
|
||||
v-model="clientNotes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Payment Stub -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<VLabel for="payment-stub">
|
||||
Payment Stub
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="payment-stub"
|
||||
v-model="paymentStub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Invoice send drawer -->
|
||||
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendSidebarActive" />
|
||||
|
||||
<!-- 👉 Invoice add payment drawer -->
|
||||
<InvoiceAddPaymentDrawer v-model:isDrawerOpen="isAddPaymentSidebarActive" />
|
||||
</VRow>
|
||||
</template>
|
454
resources/js/pages/apps/invoice/list/index.vue
Normal file
454
resources/js/pages/apps/invoice/list/index.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<script setup>
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref(null)
|
||||
const selectedRows = ref([])
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Clients',
|
||||
value: 24,
|
||||
icon: 'ri-user-line',
|
||||
},
|
||||
{
|
||||
title: 'Invoices',
|
||||
value: 165,
|
||||
icon: 'ri-pages-line',
|
||||
},
|
||||
{
|
||||
title: 'Paid',
|
||||
value: '$2.46k',
|
||||
icon: 'ri-wallet-line',
|
||||
},
|
||||
{
|
||||
title: 'Unpaid',
|
||||
value: '$876',
|
||||
icon: 'ri-money-dollar-circle-line',
|
||||
},
|
||||
])
|
||||
|
||||
// 👉 headers
|
||||
const headers = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'trending',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Client',
|
||||
key: 'client',
|
||||
},
|
||||
{
|
||||
title: 'Total',
|
||||
key: 'total',
|
||||
},
|
||||
{
|
||||
title: 'Issued Date',
|
||||
key: 'date',
|
||||
},
|
||||
{
|
||||
title: 'Balance',
|
||||
key: 'balance',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
data: invoiceData,
|
||||
execute: fetchInvoices,
|
||||
} = await useApi(createUrl('/apps/invoice', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const invoices = computed(() => invoiceData.value.invoices)
|
||||
const totalInvoices = computed(() => invoiceData.value.totalInvoices)
|
||||
|
||||
// 👉 Invoice balance variant resolver
|
||||
const resolveInvoiceBalanceVariant = (balance, total) => {
|
||||
if (balance === total)
|
||||
return {
|
||||
status: 'Unpaid',
|
||||
chip: { color: 'error' },
|
||||
}
|
||||
if (balance === 0)
|
||||
return {
|
||||
status: 'Paid',
|
||||
chip: { color: 'success' },
|
||||
}
|
||||
|
||||
return {
|
||||
status: balance,
|
||||
chip: { variant: 'text' },
|
||||
}
|
||||
}
|
||||
|
||||
const resolveInvoiceStatusVariantAndIcon = status => {
|
||||
if (status === 'Partial Payment')
|
||||
return {
|
||||
variant: 'warning',
|
||||
icon: 'ri-line-chart-line',
|
||||
}
|
||||
if (status === 'Paid')
|
||||
return {
|
||||
variant: 'success',
|
||||
icon: 'ri-check-line',
|
||||
}
|
||||
if (status === 'Downloaded')
|
||||
return {
|
||||
variant: 'info',
|
||||
icon: 'ri-arrow-down-line',
|
||||
}
|
||||
if (status === 'Draft')
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'ri-save-line',
|
||||
}
|
||||
if (status === 'Sent')
|
||||
return {
|
||||
variant: 'primary',
|
||||
icon: 'ri-mail-line',
|
||||
}
|
||||
if (status === 'Past Due')
|
||||
return {
|
||||
variant: 'error',
|
||||
icon: 'ri-error-warning-line',
|
||||
}
|
||||
|
||||
return {
|
||||
variant: 'secondary',
|
||||
icon: 'ri-close-line',
|
||||
}
|
||||
}
|
||||
|
||||
const computedMoreList = computed(() => {
|
||||
return paramId => [
|
||||
{
|
||||
title: 'Download',
|
||||
value: 'download',
|
||||
prependIcon: 'ri-download-line',
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
value: 'edit',
|
||||
prependIcon: 'ri-pencil-line',
|
||||
to: {
|
||||
name: 'apps-invoice-edit-id',
|
||||
params: { id: paramId },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
prependIcon: 'ri-stack-line',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const deleteInvoice = async id => {
|
||||
await $api(`/apps/invoice/${ id }`, { method: 'DELETE' })
|
||||
fetchInvoices()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="invoices">
|
||||
<!-- 👉 Invoice Widgets -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText class="px-2">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
class="px-6"
|
||||
:class="id !== widgetData.length - 1 && $vuetify.display.width <= 600 ? 'border-b' : ''"
|
||||
>
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex flex-column">
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
<span class="text-body-1 text-capitalize">{{ data.title }}</span>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="42"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="26"
|
||||
color="high-emphasis"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider
|
||||
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
|
||||
: $vuetify.display.smAndUp ? id % 2 === 0
|
||||
: false"
|
||||
vertical
|
||||
inset
|
||||
/>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard id="invoice-list">
|
||||
<VCardText class="d-flex align-center flex-wrap gap-4">
|
||||
<!-- 👉 Create invoice -->
|
||||
<VBtn
|
||||
prepend-icon="ri-add-line"
|
||||
:to="{ name: 'apps-invoice-add' }"
|
||||
>
|
||||
Create invoice
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-4">
|
||||
<!-- 👉 Search -->
|
||||
<div class="invoice-list-search">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
density="compact"
|
||||
placeholder="Search Invoice"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="invoice-list-search">
|
||||
<VSelect
|
||||
v-model="selectedStatus"
|
||||
placeholder="Invoice Status"
|
||||
clearable
|
||||
density="compact"
|
||||
clear-icon="ri-close-line"
|
||||
:items="['Downloaded', 'Draft', 'Sent', 'Paid', 'Partial Payment', 'Past Due']"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- SECTION Datatable -->
|
||||
<VDataTableServer
|
||||
v-model="selectedRows"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
show-select
|
||||
:items-length="totalInvoices"
|
||||
:headers="headers"
|
||||
:items="invoices"
|
||||
item-value="id"
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- id -->
|
||||
<template #item.id="{ item }">
|
||||
<RouterLink :to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }">
|
||||
#{{ item.id }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- trending -->
|
||||
<template #item.trending="{ item }">
|
||||
<VTooltip>
|
||||
<template #activator="{ props }">
|
||||
<VAvatar
|
||||
:size="28"
|
||||
v-bind="props"
|
||||
:color="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon
|
||||
:size="16"
|
||||
:icon="resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).icon"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<p class="mb-0">
|
||||
{{ item.invoiceStatus }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
Balance: {{ item.balance }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
Due date: {{ item.dueDate }}
|
||||
</p>
|
||||
</VTooltip>
|
||||
</template>
|
||||
|
||||
<!-- client -->
|
||||
<template #item.client="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:color="!item.avatar.length ? resolveInvoiceStatusVariantAndIcon(item.invoiceStatus).variant : undefined"
|
||||
:variant="!item.avatar.length ? 'tonal' : undefined"
|
||||
class="me-3"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar.length"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.client.name) }}</span>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'pages-user-profile-tab', params: { tab: 'profile' } }"
|
||||
class="text-h6 font-weight-medium mb-0"
|
||||
>
|
||||
{{ item.client.name }}
|
||||
</RouterLink>
|
||||
<span class="text-body-2">{{ item.client.companyEmail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Total -->
|
||||
<template #item.total="{ item }">
|
||||
${{ item.total }}
|
||||
</template>
|
||||
|
||||
<!-- Date -->
|
||||
<template #item.date="{ item }">
|
||||
{{ item.issuedDate }}
|
||||
</template>
|
||||
|
||||
<!-- Balance -->
|
||||
<template #item.balance="{ item }">
|
||||
<VChip
|
||||
v-if="typeof ((resolveInvoiceBalanceVariant(item.balance, item.total)).status) === 'string'"
|
||||
:color="resolveInvoiceBalanceVariant(item.balance, item.total).chip.color"
|
||||
size="small"
|
||||
>
|
||||
{{ (resolveInvoiceBalanceVariant(item.balance, item.total)).status }}
|
||||
</VChip>
|
||||
<h6
|
||||
v-else
|
||||
class="text-h6 font-weight-regular"
|
||||
>
|
||||
{{ Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status) > 0 ? `$${(resolveInvoiceBalanceVariant(item.balance, item.total)).status}` : `-$${Math.abs(Number((resolveInvoiceBalanceVariant(item.balance, item.total)).status))}` }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<div class="text-no-wrap">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteInvoice(item.id)"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
:to="{ name: 'apps-invoice-preview-id', params: { id: item.id } }"
|
||||
>
|
||||
<VIcon icon="ri-eye-line" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn
|
||||
size="small"
|
||||
:menu-list="computedMoreList(item.id)"
|
||||
item-props
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalInvoices) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalInvoices / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalInvoices / itemsPerPage) ? page = Math.ceil(totalInvoices / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
<!-- !SECTION -->
|
||||
</VCard>
|
||||
</section>
|
||||
<section v-else>
|
||||
<VCard>
|
||||
<VCardTitle>No Invoice Found</VCardTitle>
|
||||
</VCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#invoice-list {
|
||||
.invoice-list-actions {
|
||||
inline-size: 8rem;
|
||||
}
|
||||
|
||||
.invoice-list-search {
|
||||
inline-size: 12rem;
|
||||
}
|
||||
}
|
||||
</style>
|
438
resources/js/pages/apps/invoice/preview/[id].vue
Normal file
438
resources/js/pages/apps/invoice/preview/[id].vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup>
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import InvoiceAddPaymentDrawer from '@/views/apps/invoice/InvoiceAddPaymentDrawer.vue'
|
||||
import InvoiceSendInvoiceDrawer from '@/views/apps/invoice/InvoiceSendInvoiceDrawer.vue'
|
||||
|
||||
const route = useRoute('apps-invoice-preview-id')
|
||||
const isAddPaymentSidebarVisible = ref(false)
|
||||
const isSendPaymentSidebarVisible = ref(false)
|
||||
const { data: invoiceData } = await useApi(`/apps/invoice/${ Number(route.params.id) }`)
|
||||
const invoice = invoiceData.value.invoice
|
||||
const paymentDetails = invoiceData.value.paymentDetails
|
||||
|
||||
const purchasedProducts = [
|
||||
{
|
||||
name: 'Premium Branding Package',
|
||||
description: 'Branding & Promotion',
|
||||
qty: 1,
|
||||
hours: 15,
|
||||
price: 32,
|
||||
},
|
||||
{
|
||||
name: 'SMM',
|
||||
description: 'Social media templates',
|
||||
qty: 1,
|
||||
hours: 14,
|
||||
price: 28,
|
||||
},
|
||||
{
|
||||
name: 'Web Design',
|
||||
description: 'Web designing package',
|
||||
qty: 1,
|
||||
hours: 12,
|
||||
price: 24,
|
||||
},
|
||||
{
|
||||
name: 'SEO',
|
||||
description: 'Search engine optimization',
|
||||
qty: 1,
|
||||
hours: 5,
|
||||
price: 22,
|
||||
},
|
||||
]
|
||||
|
||||
const printInvoice = () => {
|
||||
window.print()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="invoiceData">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="9"
|
||||
>
|
||||
<VCard class="invoice-preview-wrapper pa-12">
|
||||
<!-- SECTION Header -->
|
||||
<div class="invoice-header-preview d-flex flex-wrap justify-space-between flex-column flex-sm-row print-row bg-var-theme-background gap-6 rounded pa-6 mb-6">
|
||||
<!-- 👉 Left Content -->
|
||||
<div>
|
||||
<div class="d-flex align-center mb-6">
|
||||
<!-- 👉 Logo -->
|
||||
<VNodeRenderer
|
||||
:nodes="themeConfig.app.logo"
|
||||
class="me-3"
|
||||
/>
|
||||
|
||||
<!-- 👉 Title -->
|
||||
<h6 class="text-xl leading-normal text-uppercase">
|
||||
{{ themeConfig.app.title }}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Address -->
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
Office 149, 450 South Brand Brooklyn
|
||||
</h6>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
San Diego County, CA 91905, USA
|
||||
</h6>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
+1 (123) 456 7891, +44 (876) 543 2198
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Right Content -->
|
||||
<div>
|
||||
<!-- 👉 Invoice ID -->
|
||||
<h6 class="font-weight-medium text-lg mb-6">
|
||||
Invoice #{{ invoice.id }}
|
||||
</h6>
|
||||
|
||||
<!-- 👉 Issue Date -->
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
<span>Date Issued: </span>
|
||||
<span>{{ new Date(invoice.issuedDate).toLocaleDateString('en-GB') }}</span>
|
||||
</h6>
|
||||
|
||||
<!-- 👉 Due Date -->
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
<span>Due Date: </span>
|
||||
<span>{{ new Date(invoice.dueDate).toLocaleDateString('en-GB') }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- 👉 Payment Details -->
|
||||
<VRow class="print-row mb-6">
|
||||
<VCol class="text-no-wrap">
|
||||
<h6 class="text-h6 mb-4">
|
||||
Invoice To:
|
||||
</h6>
|
||||
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.name }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.company }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.address }}, {{ invoice.client.country }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.contact }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{{ invoice.client.companyEmail }}
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<VCol class="text-no-wrap">
|
||||
<h6 class="text-h6 mb-4">
|
||||
Bill To:
|
||||
</h6>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Total Due:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.totalDue }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Bank Name:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.bankName }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
Country:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.country }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
IBAN:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.iban }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-6">
|
||||
SWIFT Code:
|
||||
</td>
|
||||
<td>
|
||||
{{ paymentDetails.swiftCode }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 invoice Table -->
|
||||
<VTable class="invoice-preview-table border text-high-emphasis overflow-hidden mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
ITEM
|
||||
</th>
|
||||
<th scope="col">
|
||||
DESCRIPTION
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="text-center"
|
||||
>
|
||||
HOURS
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="text-center"
|
||||
>
|
||||
QTY
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="text-center"
|
||||
>
|
||||
TOTAL
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in purchasedProducts"
|
||||
:key="item.name"
|
||||
>
|
||||
<td class="text-no-wrap">
|
||||
{{ item.name }}
|
||||
</td>
|
||||
<td class="text-no-wrap">
|
||||
{{ item.description }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.hours }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ item.qty }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
${{ item.price }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<!-- 👉 Total -->
|
||||
<div class="d-flex justify-space-between flex-column flex-sm-row print-row">
|
||||
<div class="mb-2">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<h6 class="text-h6 me-2">
|
||||
Salesperson:
|
||||
</h6>
|
||||
<span>Jenny Parker</span>
|
||||
</div>
|
||||
<p>Thanks for your business</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<table class="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Subtotal:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-sm">
|
||||
$1800
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Discount:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-sm">
|
||||
$28
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Tax:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-sm">
|
||||
21%
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<VDivider class="mt-4 mb-3" />
|
||||
|
||||
<table class="w-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pe-16">
|
||||
Total:
|
||||
</td>
|
||||
<td :class="$vuetify.locale.isRtl ? 'text-start' : 'text-end'">
|
||||
<h6 class="text-sm">
|
||||
$1690
|
||||
</h6>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VDivider class="my-6 border-dashed" />
|
||||
|
||||
<p class="mb-0">
|
||||
<span class="text-high-emphasis font-weight-medium me-1">
|
||||
Note:
|
||||
</span>
|
||||
<span>It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance projects. Thank You!</span>
|
||||
</p>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
class="d-print-none"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<!-- 👉 Send Invoice Trigger button -->
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="ri-send-plane-line"
|
||||
class="mb-4"
|
||||
@click="isSendPaymentSidebarVisible = true"
|
||||
>
|
||||
Send Invoice
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
>
|
||||
Download
|
||||
</VBtn>
|
||||
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
class="flex-grow-1"
|
||||
@click="printInvoice"
|
||||
>
|
||||
Print
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mb-4 flex-grow-1"
|
||||
:to="{ name: 'apps-invoice-edit-id', params: { id: route.params.id } }"
|
||||
>
|
||||
Edit
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Add Payment trigger button -->
|
||||
<VBtn
|
||||
block
|
||||
prepend-icon="ri-money-dollar-circle-line"
|
||||
color="success"
|
||||
@click="isAddPaymentSidebarVisible = true"
|
||||
>
|
||||
Add Payment
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 👉 Add Payment Sidebar -->
|
||||
<InvoiceAddPaymentDrawer v-model:isDrawerOpen="isAddPaymentSidebarVisible" />
|
||||
|
||||
<!-- 👉 Send Invoice Sidebar -->
|
||||
<InvoiceSendInvoiceDrawer v-model:isDrawerOpen="isSendPaymentSidebarVisible" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice-preview-table {
|
||||
--v-table-header-color: var(--v-theme-surface);
|
||||
|
||||
&.v-table .v-table__wrapper table thead tr th{
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.v-theme--dark {
|
||||
--v-theme-surface: 255, 255, 255;
|
||||
--v-theme-on-surface: 94, 86, 105;
|
||||
}
|
||||
|
||||
body {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.invoice-header-preview,
|
||||
.invoice-preview-wrapper {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.product-buy-now {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-navigation-drawer,
|
||||
.layout-vertical-nav,
|
||||
.app-customizer-toggler,
|
||||
.layout-footer,
|
||||
.layout-navbar,
|
||||
.layout-navbar-and-nav-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
box-shadow: none !important;
|
||||
|
||||
.print-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content-wrapper {
|
||||
padding-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.v-table__wrapper {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
</style>
|
121
resources/js/pages/apps/logistics/LogisticsCardStatistics.vue
Normal file
121
resources/js/pages/apps/logistics/LogisticsCardStatistics.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
});
|
||||
const logisticData = ref([
|
||||
{
|
||||
icon: 'ri-car-line',
|
||||
color: 'primary',
|
||||
title: 'Patients',
|
||||
value: 42,
|
||||
change: 18.2,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-alert-line',
|
||||
color: 'warning',
|
||||
title: 'Providers',
|
||||
value: 8,
|
||||
change: -8.7,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-stackshare-line',
|
||||
color: 'error',
|
||||
title: 'Orders',
|
||||
value: 27,
|
||||
change: 4.3,
|
||||
isHover: false,
|
||||
},
|
||||
{
|
||||
icon: 'ri-timer-line',
|
||||
color: 'info',
|
||||
title: 'Products',
|
||||
value: 13,
|
||||
change: -2.5,
|
||||
isHover: false,
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(()=>{
|
||||
console.log('data',props.data);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="(data, index) in logisticData"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="3"
|
||||
sm="6"
|
||||
>
|
||||
<div>
|
||||
<VCard
|
||||
class="logistics-card-statistics cursor-pointer"
|
||||
:style="data.isHover ? `border-block-end-color: rgb(var(--v-theme-${data.color}))` : `border-block-end-color: rgba(var(--v-theme-${data.color}),0.7)`"
|
||||
@mouseenter="data.isHover = true"
|
||||
@mouseleave="data.isHover = false"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center gap-x-4 mb-2">
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
:color="data.color"
|
||||
rounded
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="24"
|
||||
/>
|
||||
</VAvatar>
|
||||
<h4 class="text-h4">
|
||||
{{ data.value }}
|
||||
</h4>
|
||||
</div>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ data.title }}
|
||||
</h6>
|
||||
<div class="d-flex align-center">
|
||||
<div class="text-body-1 font-weight-medium me-2">
|
||||
{{ data.change }}%
|
||||
</div>
|
||||
<span class="text-sm text-disabled">than last week</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@core-scss/base/mixins" as mixins;
|
||||
|
||||
.logistics-card-statistics {
|
||||
border-block-end-style: solid;
|
||||
border-block-end-width: 2px;
|
||||
|
||||
&:hover {
|
||||
border-block-end-width: 3px;
|
||||
margin-block-end: -1px;
|
||||
|
||||
@include mixins.elevation(10);
|
||||
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.skin--bordered{
|
||||
.logistics-card-statistics {
|
||||
&:hover {
|
||||
margin-block-end: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
131
resources/js/pages/apps/logistics/dashboard.vue
Normal file
131
resources/js/pages/apps/logistics/dashboard.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup>
|
||||
import AnalyticsAward from '@/views/dashboards/analytics/AnalyticsAward.vue'
|
||||
import AnalyticsDepositWithdraw from '@/views/dashboards/analytics/AnalyticsDepositWithdraw.vue'
|
||||
import AnalyticsPerformance from '@/views/dashboards/analytics/AnalyticsPerformance.vue'
|
||||
import AnalyticsSalesByCountries from '@/views/dashboards/analytics/AnalyticsSalesByCountries.vue'
|
||||
import AnalyticsSessionBarCharts from '@/views/dashboards/analytics/AnalyticsSessionsBarCharts.vue'
|
||||
import AnalyticsTotalEarning from '@/views/dashboards/analytics/AnalyticsTotalEarning.vue'
|
||||
import AnalyticsTotalProfit from '@/views/dashboards/analytics/AnalyticsTotalProfit.vue'
|
||||
import AnalyticsTransactions from '@/views/dashboards/analytics/AnalyticsTransactions.vue'
|
||||
import AnalyticsUserTable from '@/views/dashboards/analytics/AnalyticsUserTable.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboards/analytics/AnalyticsWeeklyOverview.vue'
|
||||
|
||||
const totalProfit = {
|
||||
title: 'Total Profit',
|
||||
color: 'secondary',
|
||||
icon: 'ri-pie-chart-2-line',
|
||||
stats: '$25.6k',
|
||||
change: 42,
|
||||
subtitle: 'Weekly Project',
|
||||
}
|
||||
|
||||
const newProject = {
|
||||
title: 'New Project',
|
||||
color: 'primary',
|
||||
icon: 'ri-file-word-2-line',
|
||||
stats: '862',
|
||||
change: -18,
|
||||
subtitle: 'Yearly Project',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsAward />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<AnalyticsTransactions />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AnalyticsWeeklyOverview />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
sm="6"
|
||||
>
|
||||
<AnalyticsTotalEarning />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<AnalyticsTotalProfit />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<CardStatisticsVertical v-bind="totalProfit" />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<CardStatisticsVertical v-bind="newProject" />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<AnalyticsSessionBarCharts />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsPerformance />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<AnalyticsDepositWithdraw />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsSalesByCountries />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<AnalyticsUserTable />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
</style>
|
378
resources/js/pages/apps/logistics/fleet.vue
Normal file
378
resources/js/pages/apps/logistics/fleet.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
// ❗ WARNING please use your access token from mapbox.com
|
||||
<script setup>
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import {
|
||||
onMounted,
|
||||
ref,
|
||||
} from 'vue'
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import fleetImg from '@images/misc/fleet-car.png'
|
||||
|
||||
const { isLeftSidebarOpen } = useResponsiveLeftSidebar()
|
||||
const accessToken = 'pk.eyJ1Ijoic29jaWFsZXhwbG9yZXIiLCJhIjoiREFQbXBISSJ9.dwFTwfSaWsHvktHrRtpydQ'
|
||||
const map = ref()
|
||||
const vuetifyDisplay = useDisplay()
|
||||
|
||||
definePage({ meta: { layoutWrapperClasses: 'layout-content-height-fixed' } })
|
||||
|
||||
const carImgs = ref([
|
||||
fleetImg,
|
||||
fleetImg,
|
||||
fleetImg,
|
||||
fleetImg,
|
||||
])
|
||||
|
||||
const refCars = ref([])
|
||||
|
||||
const showPanel = ref([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
])
|
||||
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
-73.999024,
|
||||
40.75249842,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
-74.03,
|
||||
40.75699842,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
-73.967524,
|
||||
40.7599842,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
-74.0325,
|
||||
40.742992,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const activeIndex = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
mapboxgl.accessToken = accessToken
|
||||
map.value = new mapboxgl.Map({
|
||||
container: 'mapContainer',
|
||||
style: 'mapbox://styles/mapbox/light-v9',
|
||||
center: [
|
||||
-73.999024,
|
||||
40.75249842,
|
||||
],
|
||||
zoom: 12.25,
|
||||
})
|
||||
for (let index = 0; index < geojson.features.length; index++)
|
||||
new mapboxgl.Marker({ element: refCars.value[index] }).setLngLat(geojson.features[index].geometry.coordinates).addTo(map.value)
|
||||
refCars.value[activeIndex.value].classList.add('marker-focus')
|
||||
})
|
||||
|
||||
const vehicleTrackingData = [
|
||||
{
|
||||
name: 'VOL-342808',
|
||||
location: 'Chelsea, NY, USA',
|
||||
progress: 88,
|
||||
driverName: 'Veronica Herman',
|
||||
},
|
||||
{
|
||||
name: 'VOL-954784',
|
||||
location: 'Lincoln Harbor, NY, USA',
|
||||
progress: 90,
|
||||
driverName: 'Myrtle Ullrich',
|
||||
},
|
||||
{
|
||||
name: 'VOL-342808',
|
||||
location: 'Midtown East, NY, USA',
|
||||
progress: 60,
|
||||
driverName: 'Barry Schowalter',
|
||||
},
|
||||
{
|
||||
name: 'VOL-343908',
|
||||
location: 'Hoboken, NY, USA',
|
||||
progress: 28,
|
||||
driverName: 'Helen Jacobs',
|
||||
},
|
||||
]
|
||||
|
||||
const flyToLocation = (geolocation, index) => {
|
||||
activeIndex.value = index
|
||||
showPanel.value.fill(false)
|
||||
showPanel.value[index] = !showPanel.value[index]
|
||||
if (vuetifyDisplay.mdAndDown.value)
|
||||
isLeftSidebarOpen.value = false
|
||||
map.value.flyTo({
|
||||
center: geolocation,
|
||||
zoom: 16,
|
||||
})
|
||||
}
|
||||
|
||||
watch(activeIndex, () => {
|
||||
refCars.value.forEach((car, index) => {
|
||||
if (index === activeIndex.value)
|
||||
car.classList.add('marker-focus')
|
||||
else
|
||||
car.classList.remove('marker-focus')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLayout class="fleet-app-layout">
|
||||
<VNavigationDrawer
|
||||
v-model="isLeftSidebarOpen"
|
||||
width="320"
|
||||
absolute
|
||||
touchless
|
||||
:border="0"
|
||||
location="start"
|
||||
>
|
||||
<VCard
|
||||
class="h-100"
|
||||
flat
|
||||
>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
Fleet
|
||||
</VCardTitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn
|
||||
class="d-lg-none"
|
||||
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
|
||||
>
|
||||
<VIcon icon="ri-close-line" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 👉 Perfect Scrollbar -->
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false, suppressScrollX: true }"
|
||||
style="block-size: calc(100% - 60px);"
|
||||
>
|
||||
<VCardText class="pt-0">
|
||||
<div
|
||||
v-for="(vehicle, index) in vehicleTrackingData"
|
||||
:key="index"
|
||||
class="mb-6"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-center justify-space-between cursor-pointer"
|
||||
@click="flyToLocation(geojson.features[index].geometry.coordinates, index)"
|
||||
>
|
||||
<div class="d-flex gap-x-4">
|
||||
<VAvatar
|
||||
icon="ri-car-line"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
/>
|
||||
<div>
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ vehicle.name }}
|
||||
</h6>
|
||||
<div class="text-body-1">
|
||||
{{ vehicle.location }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<IconBtn density="comfortable">
|
||||
<VIcon :icon="showPanel[index] ? 'ri-arrow-down-s-line' : $vuetify.locale.isRtl ? 'ri-arrow-left-s-line' : 'ri-arrow-right-s-line'" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VExpandTransition mode="out-in">
|
||||
<div v-show="showPanel[index]">
|
||||
<div class="py-4 mb-4">
|
||||
<div class="d-flex justify-space-between text-body-1 mb-1">
|
||||
<span class="text-high-emphasis ">Delivery Process</span>
|
||||
<span>{{ vehicle.progress }}%</span>
|
||||
</div>
|
||||
<VProgressLinear
|
||||
:model-value="vehicle.progress"
|
||||
color="primary"
|
||||
rounded
|
||||
height="6"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<VTimeline
|
||||
side="end"
|
||||
align="start"
|
||||
truncate-line="both"
|
||||
density="compact"
|
||||
class="v-timeline--variant-outlined"
|
||||
>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgb(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
TRACKING NUMBER CREATED
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
{{ vehicle.driverName }}
|
||||
</div>
|
||||
<div class="text-body-2 mb-1">
|
||||
Sep 01, 7:53 AM
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
<VTimelineItem
|
||||
icon="ri-checkbox-circle-line"
|
||||
dot-color="rgb(var(--v-theme-surface))"
|
||||
icon-color="success"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-success">
|
||||
OUT FOR DELIVERY
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="app-timeline-text mb-1">
|
||||
Sep 03, 8:02 AM
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
<VTimelineItem
|
||||
icon="ri-map-pin-line"
|
||||
dot-color="rgb(var(--v-theme-surface))"
|
||||
icon-color="primary"
|
||||
fill-dot
|
||||
size="20"
|
||||
:elevation="0"
|
||||
>
|
||||
<div class="text-caption text-uppercase text-primary">
|
||||
ARRIVED
|
||||
</div>
|
||||
<div class="app-timeline-title">
|
||||
Veronica Herman
|
||||
</div>
|
||||
<div class="app-timeline-text">
|
||||
Sep 04, 8:18 AM
|
||||
</div>
|
||||
</VTimelineItem>
|
||||
</VTimeline>
|
||||
</div>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</div>
|
||||
</VCardText>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VNavigationDrawer>
|
||||
|
||||
<VMain>
|
||||
<div class="h-100">
|
||||
<IconBtn
|
||||
class="d-lg-none navigation-toggle-btn rounded-sm"
|
||||
variant="elevated"
|
||||
@click="isLeftSidebarOpen = true"
|
||||
>
|
||||
<VIcon icon="ri-menu-line" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Fleet map -->
|
||||
<div
|
||||
id="mapContainer"
|
||||
class="basemap"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-for="(car, index) in carImgs"
|
||||
:key="index"
|
||||
ref="refCars"
|
||||
:src="car"
|
||||
alt="car Img marker"
|
||||
height="42"
|
||||
width="20"
|
||||
>
|
||||
</div>
|
||||
</VMain>
|
||||
</VLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/vuetify.scss";
|
||||
@use "@core-scss/base/mixins.scss";
|
||||
@import "mapbox-gl/dist/mapbox-gl.css";
|
||||
|
||||
.fleet-app-layout {
|
||||
border-radius: vuetify.$card-border-radius;
|
||||
|
||||
@include mixins.elevation(vuetify.$card-elevation);
|
||||
|
||||
$sel-fleet-app-layout: &;
|
||||
|
||||
@at-root {
|
||||
.skin--bordered {
|
||||
@include mixins.bordered-skin($sel-fleet-app-layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-toggle-btn{
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset-block-start: 1rem;
|
||||
inset-inline-start: 1rem;
|
||||
}
|
||||
|
||||
.navigation-close-btn{
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset-block-start: 1rem;
|
||||
inset-inline-end: 1rem;
|
||||
}
|
||||
|
||||
.basemap {
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.marker-focus {
|
||||
filter: drop-shadow(0 0 7px rgb(var(--v-theme-primary)));
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-bottom-left,
|
||||
.mapboxgl-ctrl-bottom-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-id-pattern */
|
||||
#mapContainer {
|
||||
block-size: 100vh !important;
|
||||
}
|
||||
</style>
|
203
resources/js/pages/apps/permissions/index.vue
Normal file
203
resources/js/pages/apps/permissions/index.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script setup>
|
||||
const headers = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Assigned To',
|
||||
key: 'assignedTo',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Created Date',
|
||||
key: 'createdDate',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const search = ref('')
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
const isPermissionDialogVisible = ref(false)
|
||||
const isAddPermissionDialogVisible = ref(false)
|
||||
const permissionName = ref('')
|
||||
|
||||
const colors = {
|
||||
'support': {
|
||||
color: 'info',
|
||||
text: 'Support',
|
||||
},
|
||||
'users': {
|
||||
color: 'success',
|
||||
text: 'Users',
|
||||
},
|
||||
'manager': {
|
||||
color: 'warning',
|
||||
text: 'Manager',
|
||||
},
|
||||
'administrator': {
|
||||
color: 'primary',
|
||||
text: 'Administrator',
|
||||
},
|
||||
'restricted-user': {
|
||||
color: 'error',
|
||||
text: 'Restricted User',
|
||||
},
|
||||
}
|
||||
|
||||
const { data: permissionsData } = await useApi(createUrl('/apps/permissions', {
|
||||
query: {
|
||||
q: search,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const permissions = computed(() => permissionsData.value.permissions)
|
||||
const totalPermissions = computed(() => permissionsData.value.totalPermissions)
|
||||
|
||||
const editPermission = name => {
|
||||
isPermissionDialogVisible.value = true
|
||||
permissionName.value = name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center justify-sm-space-between justify-start gap-4 flex-wrap">
|
||||
<VTextField
|
||||
v-model="search"
|
||||
density="compact"
|
||||
placeholder="Search Permission"
|
||||
style="max-inline-size: 15rem;min-inline-size: 12rem;"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
density="default"
|
||||
@click="isAddPermissionDialogVisible = true"
|
||||
>
|
||||
Add Permission
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:items-length="totalPermissions"
|
||||
:items-per-page-options="[
|
||||
{ value: 5, title: '5' },
|
||||
{ value: 10, title: '10' },
|
||||
{ value: -1, title: '$vuetify.dataFooter.itemsPerPageAll' },
|
||||
]"
|
||||
:headers="headers"
|
||||
:items="permissions"
|
||||
item-value="name"
|
||||
class="text-no-wrap"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- Assigned To -->
|
||||
<template #item.assignedTo="{ item }">
|
||||
<div class="d-flex gap-2">
|
||||
<VChip
|
||||
v-for="text in item.assignedTo"
|
||||
:key="text"
|
||||
:color="colors[text].color"
|
||||
size="small"
|
||||
>
|
||||
{{ colors[text].text }}
|
||||
</VChip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Name -->
|
||||
<template #item.name="{ item }">
|
||||
<h6 class="text-h6 font-weight-regular">
|
||||
{{ item.name }}
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<template #item.createdDate="{ item }">
|
||||
<span class="text-body-1">{{ item.createdDate }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="editPermission(item.name)"
|
||||
>
|
||||
<VIcon icon="ri-edit-box-line" />
|
||||
</IconBtn>
|
||||
|
||||
<MoreBtn size="small" />
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalPermissions) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalPermissions / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalPermissions / itemsPerPage) ? page = Math.ceil(totalPermissions / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
|
||||
<AddEditPermissionDialog
|
||||
v-model:isDialogVisible="isPermissionDialogVisible"
|
||||
v-model:permission-name="permissionName"
|
||||
/>
|
||||
<AddEditPermissionDialog v-model:isDialogVisible="isAddPermissionDialogVisible" />
|
||||
</template>
|
34
resources/js/pages/apps/roles/index.vue
Normal file
34
resources/js/pages/apps/roles/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import RoleCards from '@/views/apps/roles/RoleCards.vue'
|
||||
import UserList from '@/views/apps/roles/UserList.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<h5 class="text-h5 mb-1">
|
||||
Roles List
|
||||
</h5>
|
||||
<p class="text-body-1 mb-0">
|
||||
A role provided access to predefined menus and features so that depending on assigned role an administrator can have access to what he need
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Roles Cards -->
|
||||
<VCol cols="12">
|
||||
<RoleCards />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<h5 class="text-h5 mt-6">
|
||||
Total users with their roles
|
||||
</h5>
|
||||
<p class="text-body-1 mb-6">
|
||||
Find all of your company's administrator accounts and their associate roles.
|
||||
</p>
|
||||
|
||||
<!-- 👉 User List -->
|
||||
<UserList />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
533
resources/js/pages/apps/user/list/index.vue
Normal file
533
resources/js/pages/apps/user/list/index.vue
Normal file
@@ -0,0 +1,533 @@
|
||||
<script setup>
|
||||
import AddNewUserDrawer from '@/views/apps/user/list/AddNewUserDrawer.vue'
|
||||
|
||||
// 👉 Store
|
||||
const searchQuery = ref('')
|
||||
const selectedRole = ref()
|
||||
const selectedPlan = ref()
|
||||
const selectedStatus = ref()
|
||||
|
||||
// Data table options
|
||||
const itemsPerPage = ref(10)
|
||||
const page = ref(1)
|
||||
const sortBy = ref()
|
||||
const orderBy = ref()
|
||||
|
||||
const updateOptions = options => {
|
||||
page.value = options.page
|
||||
sortBy.value = options.sortBy[0]?.key
|
||||
orderBy.value = options.sortBy[0]?.order
|
||||
}
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{
|
||||
title: 'User',
|
||||
key: 'user',
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: 'Role',
|
||||
key: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Plan',
|
||||
key: 'plan',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
data: usersData,
|
||||
execute: fetchUsers,
|
||||
} = await useApi(createUrl('/apps/users', {
|
||||
query: {
|
||||
q: searchQuery,
|
||||
status: selectedStatus,
|
||||
plan: selectedPlan,
|
||||
role: selectedRole,
|
||||
itemsPerPage,
|
||||
page,
|
||||
sortBy,
|
||||
orderBy,
|
||||
},
|
||||
}))
|
||||
|
||||
const users = computed(() => usersData.value.users)
|
||||
const totalUsers = computed(() => usersData.value.totalUsers)
|
||||
|
||||
// 👉 search filters
|
||||
const roles = [
|
||||
{
|
||||
title: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
title: 'Author',
|
||||
value: 'author',
|
||||
},
|
||||
{
|
||||
title: 'Editor',
|
||||
value: 'editor',
|
||||
},
|
||||
{
|
||||
title: 'Maintainer',
|
||||
value: 'maintainer',
|
||||
},
|
||||
{
|
||||
title: 'Subscriber',
|
||||
value: 'subscriber',
|
||||
},
|
||||
]
|
||||
|
||||
const plans = [
|
||||
{
|
||||
title: 'Basic',
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
value: 'company',
|
||||
},
|
||||
{
|
||||
title: 'Enterprise',
|
||||
value: 'enterprise',
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
value: 'team',
|
||||
},
|
||||
]
|
||||
|
||||
const status = [
|
||||
{
|
||||
title: 'Pending',
|
||||
value: 'pending',
|
||||
},
|
||||
{
|
||||
title: 'Active',
|
||||
value: 'active',
|
||||
},
|
||||
{
|
||||
title: 'Inactive',
|
||||
value: 'inactive',
|
||||
},
|
||||
]
|
||||
|
||||
const resolveUserRoleVariant = role => {
|
||||
const roleLowerCase = role.toLowerCase()
|
||||
if (roleLowerCase === 'subscriber')
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
if (roleLowerCase === 'author')
|
||||
return {
|
||||
color: 'error',
|
||||
icon: 'ri-computer-line',
|
||||
}
|
||||
if (roleLowerCase === 'maintainer')
|
||||
return {
|
||||
color: 'info',
|
||||
icon: 'ri-pie-chart-line',
|
||||
}
|
||||
if (roleLowerCase === 'editor')
|
||||
return {
|
||||
color: 'warning',
|
||||
icon: 'ri-edit-box-line',
|
||||
}
|
||||
if (roleLowerCase === 'admin')
|
||||
return {
|
||||
color: 'primary',
|
||||
icon: 'ri-vip-crown-line',
|
||||
}
|
||||
|
||||
return {
|
||||
color: 'success',
|
||||
icon: 'ri-user-line',
|
||||
}
|
||||
}
|
||||
|
||||
const resolveUserStatusVariant = stat => {
|
||||
const statLowerCase = stat.toLowerCase()
|
||||
if (statLowerCase === 'pending')
|
||||
return 'warning'
|
||||
if (statLowerCase === 'active')
|
||||
return 'success'
|
||||
if (statLowerCase === 'inactive')
|
||||
return 'secondary'
|
||||
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
const isAddNewUserDrawerVisible = ref(false)
|
||||
|
||||
const addNewUser = async userData => {
|
||||
|
||||
// userListStore.addUser(userData)
|
||||
await $api('/apps/users', {
|
||||
method: 'POST',
|
||||
body: userData,
|
||||
})
|
||||
|
||||
// refetch User
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
const deleteUser = async id => {
|
||||
await $api(`/apps/users/${ id }`, { method: 'DELETE' })
|
||||
|
||||
// refetch User
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
const widgetData = ref([
|
||||
{
|
||||
title: 'Session',
|
||||
value: '21,459',
|
||||
change: 29,
|
||||
desc: 'Total Users',
|
||||
icon: 'ri-group-line',
|
||||
iconColor: 'primary',
|
||||
},
|
||||
{
|
||||
title: 'Paid Users',
|
||||
value: '4,567',
|
||||
change: 18,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'ri-user-add-line',
|
||||
iconColor: 'error',
|
||||
},
|
||||
{
|
||||
title: 'Active Users',
|
||||
value: '19,860',
|
||||
change: -14,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'ri-user-follow-line',
|
||||
iconColor: 'success',
|
||||
},
|
||||
{
|
||||
title: 'Pending Users',
|
||||
value: '237',
|
||||
change: 42,
|
||||
desc: 'Last Week Analytics',
|
||||
icon: 'ri-user-search-line',
|
||||
iconColor: 'warning',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<!-- 👉 Widgets -->
|
||||
<div class="d-flex mb-6">
|
||||
<VRow>
|
||||
<template
|
||||
v-for="(data, id) in widgetData"
|
||||
:key="id"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
sm="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex flex-column gap-y-1">
|
||||
<span class="text-base text-high-emphasis">{{ data.title }}</span>
|
||||
<h4 class="text-h4 d-flex align-center gap-2">
|
||||
{{ data.value }}
|
||||
<span
|
||||
class="text-base font-weight-regular"
|
||||
:class="data.change > 0 ? 'text-success' : 'text-error'"
|
||||
>({{ prefixWithPlus(data.change) }}%)</span>
|
||||
</h4>
|
||||
|
||||
<p class="text-sm mb-0">
|
||||
{{ data.desc }}
|
||||
</p>
|
||||
</div>
|
||||
<VAvatar
|
||||
:color="data.iconColor"
|
||||
variant="tonal"
|
||||
rounded
|
||||
size="42"
|
||||
>
|
||||
<VIcon
|
||||
:icon="data.icon"
|
||||
size="26"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</div>
|
||||
|
||||
<VCard
|
||||
title="Filters"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- 👉 Select Role -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedRole"
|
||||
label="Select Role"
|
||||
placeholder="Select Role"
|
||||
:items="roles"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- 👉 Select Plan -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedPlan"
|
||||
label="Select Plan"
|
||||
placeholder="Select Plan"
|
||||
:items="plans"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- 👉 Select Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="selectedStatus"
|
||||
label="Select Status"
|
||||
placeholder="Select Status"
|
||||
:items="status"
|
||||
clearable
|
||||
clear-icon="ri-close-line"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex flex-wrap gap-4">
|
||||
<!-- 👉 Export button -->
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prepend-icon="ri-upload-2-line"
|
||||
>
|
||||
Export
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<div class="app-user-search-filter d-flex align-center">
|
||||
<!-- 👉 Search -->
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search User"
|
||||
density="compact"
|
||||
class="me-4"
|
||||
/>
|
||||
<!-- 👉 Add user button -->
|
||||
<VBtn @click="isAddNewUserDrawerVisible = true">
|
||||
Add New User
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- SECTION datatable -->
|
||||
<VDataTableServer
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
v-model:page="page"
|
||||
:items="users"
|
||||
item-value="id"
|
||||
:items-length="totalUsers"
|
||||
:headers="headers"
|
||||
show-select
|
||||
class="text-no-wrap rounded-0"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<!-- User -->
|
||||
<template #item.user="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:variant="!item.avatar ? 'tonal' : undefined"
|
||||
:color="!item.avatar ? resolveUserRoleVariant(item.role).color : undefined"
|
||||
class="me-3"
|
||||
>
|
||||
<VImg
|
||||
v-if="item.avatar"
|
||||
:src="item.avatar"
|
||||
/>
|
||||
<span v-else>{{ avatarText(item.fullName) }}</span>
|
||||
</VAvatar>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<RouterLink
|
||||
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
|
||||
class="text-h6 font-weight-medium user-list-name"
|
||||
>
|
||||
{{ item.fullName }}
|
||||
</RouterLink>
|
||||
|
||||
<span class="text-sm text-medium-emphasis">@{{ item.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Role -->
|
||||
<template #item.role="{ item }">
|
||||
<div class="d-flex gap-4">
|
||||
<VIcon
|
||||
:icon="resolveUserRoleVariant(item.role).icon"
|
||||
:color="resolveUserRoleVariant(item.role).color"
|
||||
/>
|
||||
<span class="text-capitalize text-high-emphasis">{{ item.role }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Plan -->
|
||||
<template #item.plan="{ item }">
|
||||
<span class="text-capitalize text-high-emphasis">{{ item.currentPlan }}</span>
|
||||
</template>
|
||||
<!-- Status -->
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="resolveUserStatusVariant(item.status)"
|
||||
size="small"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="deleteUser(item.id)"
|
||||
>
|
||||
<VIcon icon="ri-delete-bin-7-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
:to="{ name: 'apps-user-view-id', params: { id: item.id } }"
|
||||
>
|
||||
<VIcon icon="ri-eye-line" />
|
||||
</IconBtn>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
color="medium-emphasis"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
icon="ri-more-2-line"
|
||||
/>
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList>
|
||||
<VListItem link>
|
||||
<template #prepend>
|
||||
<VIcon icon="ri-download-line" />
|
||||
</template>
|
||||
<VListItemTitle>Download</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem link>
|
||||
<template #prepend>
|
||||
<VIcon icon="ri-edit-box-line" />
|
||||
</template>
|
||||
<VListItemTitle>Edit</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template #bottom>
|
||||
<VDivider />
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-x-6 px-2 py-1">
|
||||
<div class="d-flex align-center gap-x-2 text-medium-emphasis text-base">
|
||||
Rows Per Page:
|
||||
<VSelect
|
||||
v-model="itemsPerPage"
|
||||
class="per-page-select"
|
||||
variant="plain"
|
||||
:items="[10, 20, 25, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="d-flex align-center text-base text-high-emphasis me-2 mb-0">
|
||||
{{ paginationMeta({ page, itemsPerPage }, totalUsers) }}
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-x-2 align-center me-2">
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-left-s-line"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
color="high-emphasis"
|
||||
:disabled="page <= 1"
|
||||
@click="page <= 1 ? page = 1 : page--"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="flip-in-rtl"
|
||||
icon="ri-arrow-right-s-line"
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="high-emphasis"
|
||||
:disabled="page >= Math.ceil(totalUsers / itemsPerPage)"
|
||||
@click="page >= Math.ceil(totalUsers / itemsPerPage) ? page = Math.ceil(totalUsers / itemsPerPage) : page++ "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
<!-- SECTION -->
|
||||
</VCard>
|
||||
<!-- 👉 Add New User -->
|
||||
<AddNewUserDrawer
|
||||
v-model:isDrawerOpen="isAddNewUserDrawerVisible"
|
||||
@user-data="addNewUser"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.app-user-search-filter {
|
||||
inline-size: 24.0625rem;
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.user-list-name:not(:hover) {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
</style>
|
101
resources/js/pages/apps/user/view/[id].vue
Normal file
101
resources/js/pages/apps/user/view/[id].vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
import UserBioPanel from '@/views/apps/user/view/UserBioPanel.vue'
|
||||
import UserTabBillingsPlans from '@/views/apps/user/view/UserTabBillingsPlans.vue'
|
||||
import UserTabConnections from '@/views/apps/user/view/UserTabConnections.vue'
|
||||
import UserTabNotifications from '@/views/apps/user/view/UserTabNotifications.vue'
|
||||
import UserTabOverview from '@/views/apps/user/view/UserTabOverview.vue'
|
||||
import UserTabSecurity from '@/views/apps/user/view/UserTabSecurity.vue'
|
||||
|
||||
const route = useRoute('apps-user-view-id')
|
||||
const userTab = ref(null)
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
icon: 'ri-group-line',
|
||||
title: 'Overview',
|
||||
},
|
||||
{
|
||||
icon: 'ri-lock-2-line',
|
||||
title: 'Security',
|
||||
},
|
||||
{
|
||||
icon: 'ri-bookmark-line',
|
||||
title: 'Billing & Plan',
|
||||
},
|
||||
{
|
||||
icon: 'ri-notification-4-line',
|
||||
title: 'Notifications',
|
||||
},
|
||||
{
|
||||
icon: 'ri-link-m',
|
||||
title: 'Connections',
|
||||
},
|
||||
]
|
||||
|
||||
const { data: userData } = await useApi(`/apps/users/${ route.params.id }`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow v-if="userData">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="5"
|
||||
lg="4"
|
||||
>
|
||||
<UserBioPanel :user-data="userData" />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="7"
|
||||
lg="8"
|
||||
>
|
||||
<VTabs
|
||||
v-model="userTab"
|
||||
class="v-tabs-pill"
|
||||
>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.icon"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
:icon="tab.icon"
|
||||
/>
|
||||
<span>{{ tab.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow
|
||||
v-model="userTab"
|
||||
class="mt-6 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem>
|
||||
<UserTabOverview />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabSecurity />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabBillingsPlans />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabNotifications />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem>
|
||||
<UserTabConnections />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard v-else>
|
||||
<VCardTitle class="text-center">
|
||||
No User Found
|
||||
</VCardTitle>
|
||||
</VCard>
|
||||
</template>
|
Reference in New Issue
Block a user