initial commit

This commit is contained in:
Inshal
2024-10-25 19:58:19 +05:00
commit 2046156f90
1558 changed files with 210706 additions and 0 deletions

View File

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

View File

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

View File

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