Initial commit
This commit is contained in:
442
resources/js/components/AnalysisCard.vue
Normal file
442
resources/js/components/AnalysisCard.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<VCard class="h-100 overflow-visible position-relative" :style="cardBackgroundStyle">
|
||||
<VCardText class="pa-6 h-100 d-flex flex-column">
|
||||
<div class="d-flex justify-center align-center mb-4 flex-wrap gap-2">
|
||||
<VChip color="primary" variant="tonal" size="small">
|
||||
<VIcon icon="tabler-calendar" size="14" class="me-1" />
|
||||
{{ getCurrentDate() }}
|
||||
</VChip>
|
||||
<VChip color="secondary" variant="outlined" size="small" v-if="chartName">
|
||||
<VIcon icon="tabler-chart-line" size="14" class="me-1" />
|
||||
{{ chartName }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-6">
|
||||
<div class="progress-circle-container mb-4">
|
||||
<VProgressCircular
|
||||
:model-value="completionPercent"
|
||||
:size="120"
|
||||
:width="8"
|
||||
color="success"
|
||||
class="progress-main"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-h3 font-weight-bold">{{ completionPercent }}%</h3>
|
||||
<span class="text-caption text-medium-emphasis">Completed</span>
|
||||
</div>
|
||||
</VProgressCircular>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<VBtn
|
||||
block
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
class="btn-analyze"
|
||||
:loading="loading"
|
||||
@click="onAnalyzeClick"
|
||||
>
|
||||
<VIcon
|
||||
:icon="loading ? 'tabler-loader-2' : 'tabler-chart-bar'"
|
||||
class="me-2"
|
||||
:class="{ 'rotating': loading }"
|
||||
/>
|
||||
{{ loading ? "Analyzing..." : "Analyze" }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1 d-flex flex-column" style="min-height: 0; height: 250px;">
|
||||
<VCard
|
||||
variant="tonal"
|
||||
color="surface"
|
||||
class="analysis-container d-flex flex-column h-100"
|
||||
>
|
||||
<VCardText class="pa-4 d-flex flex-column h-100" style="min-height: 0;">
|
||||
<div class="d-flex align-center mb-3">
|
||||
<VIcon icon="tabler-report-analytics" class="me-2" />
|
||||
<h6 class="text-h6">Analysis Results {{ chartName ? `- ${chartName}` : '' }}</h6>
|
||||
</div>
|
||||
<div
|
||||
class="analysis-content custom-scrollbar"
|
||||
style="overflow-y: auto; height: 1px; flex: 1; color: white;"
|
||||
v-html="analysisResult"
|
||||
></div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
|
||||
// Define props
|
||||
const props = defineProps({
|
||||
chartName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
const loading = ref(false)
|
||||
const completionPercent = ref(25)
|
||||
const analysisResult = ref(`
|
||||
<div class="text-center text-medium-emphasis py-8">
|
||||
<div class="mb-3">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mb-0">Click 'Analyze ${props.chartName || 'Projects'}' to generate detailed insights</p>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const getCurrentDate = () => {
|
||||
const today = new Date()
|
||||
const options = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}
|
||||
return today.toLocaleDateString('en-US', options)
|
||||
}
|
||||
|
||||
const cardBackgroundStyle = computed(() => {
|
||||
const currentTheme = vuetifyTheme.current.value.colors
|
||||
const createGradientColor = (color, opacity = 0.08) =>
|
||||
color.startsWith('#') ? `rgba(${hexToRgb(color)}, ${opacity})` : `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
|
||||
return {
|
||||
background: `linear-gradient(135deg,
|
||||
${createGradientColor(currentTheme.primary, 0.12)} 0%,
|
||||
${createGradientColor(currentTheme.success, 0.06)} 50%,
|
||||
${createGradientColor(currentTheme.primary, 0.04)} 100%)`
|
||||
}
|
||||
})
|
||||
|
||||
function onAnalyzeClick() {
|
||||
loading.value = true
|
||||
analysisResult.value = `
|
||||
<div class="text-center py-4">
|
||||
<div class="mb-3">
|
||||
<svg class="rotating" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-medium-emphasis">Analyzing ${props.chartName || 'project'} data...</p>
|
||||
</div>
|
||||
`
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
analysisResult.value = `
|
||||
<div class="analysis-results">
|
||||
<div class="result-section mb-4">
|
||||
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
<span class="result-icon success me-2">📊</span>
|
||||
${props.chartName || 'Project'} Overview
|
||||
</h6>
|
||||
<div class="result-grid">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Avg. Completion Time</span>
|
||||
<span class="result-value text-primary">14 days</span>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Success Rate</span>
|
||||
<span class="result-value text-success">85%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-section mb-4">
|
||||
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
<span class="result-icon warning me-2">📈</span>
|
||||
Current Status
|
||||
</h6>
|
||||
<div class="status-list">
|
||||
<div class="status-item">
|
||||
<div class="status-dot success"></div>
|
||||
<span>Completed ${props.chartName ? props.chartName.toLowerCase() : 'projects'}: <strong>1</strong></span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-dot warning"></div>
|
||||
<span>Pending ${props.chartName ? props.chartName.toLowerCase() : 'projects'}: <strong>2</strong></span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-dot primary"></div>
|
||||
<span>Average Progress: <strong>40%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-section mb-4">
|
||||
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
<span class="result-icon info me-2">💡</span>
|
||||
Recommendations
|
||||
</h6>
|
||||
<ul class="recommendation-list">
|
||||
<li>Focus on completing pending ${props.chartName ? props.chartName.toLowerCase() : 'projects'}</li>
|
||||
<li>Optimize resource allocation for better efficiency</li>
|
||||
<li>Set up automated progress tracking</li>
|
||||
<li>Review ${props.chartName ? props.chartName.toLowerCase() : 'project'} timelines and deadlines</li>
|
||||
<li>Implement better team communication</li>
|
||||
<li>Consider hiring additional resources</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="result-section mb-4">
|
||||
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
<span class="result-icon me-2">📅</span>
|
||||
Detailed Timeline
|
||||
</h6>
|
||||
<div class="timeline-items">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<strong>Alpha ${props.chartName || 'Project'}</strong> - Started 10 days ago, 80% complete
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<strong>Beta ${props.chartName || 'Project'}</strong> - Started 5 days ago, 30% complete
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<strong>Gamma ${props.chartName || 'Project'}</strong> - Starting next week, 0% complete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-section">
|
||||
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
<span class="result-icon me-2">⚡</span>
|
||||
Performance Metrics
|
||||
</h6>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Tasks per Day</span>
|
||||
<span class="metric-value">3.2</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Efficiency Score</span>
|
||||
<span class="metric-value">92%</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Team Velocity</span>
|
||||
<span class="metric-value">High</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Quality Score</span>
|
||||
<span class="metric-value">8.7/10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-circle-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.floating-stats {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-chip {
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-analyze {
|
||||
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-analyze:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(var(--v-theme-primary), 0.4);
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(var(--v-border-color), 0.1);
|
||||
}
|
||||
|
||||
.analysis-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(var(--v-theme-primary), 0.3) transparent;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.analysis-content * {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.analysis-content .text-medium-emphasis {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.analysis-content .text-primary {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.analysis-content .text-success {
|
||||
color: rgb(var(--v-theme-success)) !important;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(var(--v-theme-primary), 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: rgba(var(--v-theme-surface), 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(var(--v-border-color), 0.1);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.success { background-color: rgb(var(--v-theme-success)); }
|
||||
.status-dot.warning { background-color: rgb(var(--v-theme-warning)); }
|
||||
.status-dot.primary { background-color: rgb(var(--v-theme-primary)); }
|
||||
|
||||
.result-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.recommendation-list {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.recommendation-list li {
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.timeline-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
</style>
|
||||
63
resources/js/components/AppLoadingIndicator.vue
Normal file
63
resources/js/components/AppLoadingIndicator.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
const bufferValue = ref(20)
|
||||
const progressValue = ref(10)
|
||||
const isFallbackState = ref(false)
|
||||
const interval = ref()
|
||||
const showProgress = ref(false)
|
||||
|
||||
watch([
|
||||
progressValue,
|
||||
isFallbackState,
|
||||
], () => {
|
||||
if (progressValue.value > 80 && isFallbackState.value)
|
||||
progressValue.value = 82
|
||||
startBuffer()
|
||||
})
|
||||
function startBuffer() {
|
||||
clearInterval(interval.value)
|
||||
interval.value = setInterval(() => {
|
||||
progressValue.value += Math.random() * (15 - 5) + 5
|
||||
bufferValue.value += Math.random() * (15 - 5) + 6
|
||||
}, 800)
|
||||
}
|
||||
|
||||
const fallbackHandle = () => {
|
||||
showProgress.value = true
|
||||
progressValue.value = 10
|
||||
isFallbackState.value = true
|
||||
startBuffer()
|
||||
}
|
||||
|
||||
const resolveHandle = () => {
|
||||
isFallbackState.value = false
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
clearInterval(interval.value)
|
||||
progressValue.value = 0
|
||||
bufferValue.value = 20
|
||||
showProgress.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
fallbackHandle,
|
||||
resolveHandle,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- loading state via #fallback slot -->
|
||||
<div
|
||||
v-if="showProgress"
|
||||
class="position-fixed"
|
||||
style="z-index: 9999; inset-block-start: 0; inset-inline: 0 0;"
|
||||
>
|
||||
<VProgressLinear
|
||||
v-model="progressValue"
|
||||
:buffer-value="bufferValue"
|
||||
color="primary"
|
||||
height="2"
|
||||
bg-color="background"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
278
resources/js/components/AppPricing.vue
Normal file
278
resources/js/components/AppPricing.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup>
|
||||
import safeBoxWithGoldenCoin from '@images/misc/3d-safe-box-with-golden-dollar-coins.png'
|
||||
import spaceRocket from '@images/misc/3d-space-rocket-with-smoke.png'
|
||||
import dollarCoinPiggyBank from '@images/misc/dollar-coins-flying-pink-piggy-bank.png'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
xs: {
|
||||
type: [
|
||||
Number,
|
||||
String,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
sm: {
|
||||
type: [
|
||||
Number,
|
||||
String,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
md: {
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
lg: {
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
xl: {
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const annualMonthlyPlanPriceToggler = ref(true)
|
||||
|
||||
const pricingPlans = [
|
||||
{
|
||||
name: 'Basic',
|
||||
tagLine: 'A simple start for everyone',
|
||||
logo: dollarCoinPiggyBank,
|
||||
monthlyPrice: 0,
|
||||
yearlyPrice: 0,
|
||||
isPopular: false,
|
||||
current: true,
|
||||
features: [
|
||||
'100 responses a month',
|
||||
'Unlimited forms and surveys',
|
||||
'Unlimited fields',
|
||||
'Basic form creation tools',
|
||||
'Up to 2 subdomains',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
tagLine: 'For small to medium businesses',
|
||||
logo: safeBoxWithGoldenCoin,
|
||||
monthlyPrice: 49,
|
||||
yearlyPrice: 499,
|
||||
isPopular: true,
|
||||
current: false,
|
||||
features: [
|
||||
'Unlimited responses',
|
||||
'Unlimited forms and surveys',
|
||||
'Instagram profile page',
|
||||
'Google Docs integration',
|
||||
'Custom “Thank you” page',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
tagLine: 'Solution for big organizations',
|
||||
logo: spaceRocket,
|
||||
monthlyPrice: 99,
|
||||
yearlyPrice: 999,
|
||||
isPopular: false,
|
||||
current: false,
|
||||
features: [
|
||||
'PayPal payments',
|
||||
'Logic Jumps',
|
||||
'File upload with 5GB storage',
|
||||
'Custom domain support',
|
||||
'Stripe integration',
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Title and subtitle -->
|
||||
<div class="text-center">
|
||||
<h3 class="text-h3 pricing-title mb-2">
|
||||
{{ props.title ? props.title : 'Pricing Plans' }}
|
||||
</h3>
|
||||
<p class="mb-0">
|
||||
All plans include 40+ advanced tools and features to boost your product.
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
Choose the best plan to fit your needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Annual and monthly price toggler -->
|
||||
|
||||
<div class="d-flex font-weight-medium text-body-1 align-center justify-center mx-auto mt-12 mb-6">
|
||||
<VLabel
|
||||
for="pricing-plan-toggle"
|
||||
class="me-3"
|
||||
>
|
||||
Monthly
|
||||
</VLabel>
|
||||
|
||||
<div class="position-relative">
|
||||
<VSwitch
|
||||
id="pricing-plan-toggle"
|
||||
v-model="annualMonthlyPlanPriceToggler"
|
||||
>
|
||||
<template #label>
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
Annually
|
||||
</div>
|
||||
</template>
|
||||
</VSwitch>
|
||||
|
||||
<div class="save-upto-chip position-absolute align-center d-none d-md-flex gap-1">
|
||||
<VIcon
|
||||
icon="tabler-corner-left-down"
|
||||
size="24"
|
||||
class="flip-in-rtl mt-2 text-disabled"
|
||||
/>
|
||||
<VChip
|
||||
label
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Save up to 10%
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION pricing plans -->
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="plan in pricingPlans"
|
||||
:key="plan.logo"
|
||||
v-bind="props"
|
||||
cols="12"
|
||||
>
|
||||
<!-- 👉 Card -->
|
||||
<VCard
|
||||
flat
|
||||
border
|
||||
:class="plan.isPopular ? 'border-primary border-opacity-100' : ''"
|
||||
>
|
||||
<VCardText
|
||||
style="block-size: 3.75rem;"
|
||||
class="text-end"
|
||||
>
|
||||
<!-- 👉 Popular -->
|
||||
<VChip
|
||||
v-show="plan.isPopular"
|
||||
label
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Popular
|
||||
</VChip>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Plan logo -->
|
||||
<VCardText>
|
||||
<VImg
|
||||
:height="120"
|
||||
:width="120"
|
||||
:src="plan.logo"
|
||||
class="mx-auto mb-5"
|
||||
/>
|
||||
|
||||
<!-- 👉 Plan name -->
|
||||
<h4 class="text-h4 mb-1 text-center">
|
||||
{{ plan.name }}
|
||||
</h4>
|
||||
<p class="mb-0 text-body-1 text-center">
|
||||
{{ plan.tagLine }}
|
||||
</p>
|
||||
|
||||
<!-- 👉 Plan price -->
|
||||
|
||||
<div class="position-relative">
|
||||
<div class="d-flex justify-center pt-5 pb-10">
|
||||
<div class="text-body-1 align-self-start font-weight-medium">
|
||||
$
|
||||
</div>
|
||||
<h1 class="text-h1 font-weight-medium text-primary">
|
||||
{{ annualMonthlyPlanPriceToggler ? Math.floor(Number(plan.yearlyPrice) / 12) : plan.monthlyPrice }}
|
||||
</h1>
|
||||
<div class="text-body-1 font-weight-medium align-self-end">
|
||||
/month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Annual Price -->
|
||||
<span
|
||||
v-show="annualMonthlyPlanPriceToggler"
|
||||
class="annual-price-text position-absolute text-caption text-disabled pb-4"
|
||||
>
|
||||
{{ plan.yearlyPrice === 0 ? 'free' : `USD ${plan.yearlyPrice}/Year` }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Plan features -->
|
||||
|
||||
<VList class="card-list mb-4">
|
||||
<VListItem
|
||||
v-for="feature in plan.features"
|
||||
:key="feature"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="8"
|
||||
icon="tabler-circle-filled"
|
||||
color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="text-body-1">
|
||||
{{ feature }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<!-- 👉 Plan actions -->
|
||||
<VBtn
|
||||
block
|
||||
:color="plan.current ? 'success' : 'primary'"
|
||||
:variant="plan.isPopular ? 'elevated' : 'tonal'"
|
||||
:to="{ name: 'front-pages-payment' }"
|
||||
:active="false"
|
||||
>
|
||||
{{ plan.yearlyPrice === 0 ? 'Your Current Plan' : 'Upgrade' }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- !SECTION -->
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 1rem;
|
||||
}
|
||||
|
||||
.save-upto-chip {
|
||||
inset-block-start: -2.4rem;
|
||||
inset-inline-end: -6rem;
|
||||
}
|
||||
|
||||
.annual-price-text {
|
||||
inset-block-end: 3%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
92
resources/js/components/AppSearchHeader.vue
Normal file
92
resources/js/components/AppSearchHeader.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import AppSearchHeaderBg from '@images/pages/app-search-header-bg.png'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
density: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'comfortable',
|
||||
},
|
||||
isReverse: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Search Banner -->
|
||||
<VCard
|
||||
flat
|
||||
class="text-center search-header"
|
||||
:class="props.customClass"
|
||||
:style="`background: url(${AppSearchHeaderBg});`"
|
||||
>
|
||||
<VCardText>
|
||||
<slot name="title">
|
||||
<h4 class="text-h4 mb-2 font-weight-medium">
|
||||
{{ props.title }}
|
||||
</h4>
|
||||
</slot>
|
||||
<div
|
||||
class="d-flex"
|
||||
:class="isReverse ? 'flex-column' : 'flex-column-reverse' "
|
||||
>
|
||||
<p class="mb-0">
|
||||
{{ props.subtitle }}
|
||||
</p>
|
||||
<!-- 👉 Search Input -->
|
||||
<div>
|
||||
<AppTextField
|
||||
v-bind="$attrs"
|
||||
class="search-header-input mx-auto my-4"
|
||||
:placeholder="props.placeholder"
|
||||
:density="props.density"
|
||||
prepend-inner-icon="tabler-search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.search-header {
|
||||
padding: 4rem !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
// search input
|
||||
.search-header-input {
|
||||
border-radius: 0.375rem !important;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
max-inline-size: 28.125rem !important;
|
||||
}
|
||||
|
||||
@media (max-width: 37.5rem) {
|
||||
.search-header {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
779
resources/js/components/ComponentsLibrary.vue
Normal file
779
resources/js/components/ComponentsLibrary.vue
Normal file
@@ -0,0 +1,779 @@
|
||||
<template>
|
||||
<div class="components-library-container">
|
||||
<!-- Toggle Button -->
|
||||
<VBtn
|
||||
v-if="!isLibraryOpen"
|
||||
@click="toggleLibrary"
|
||||
class="library-toggle-btn"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
>
|
||||
<VIcon icon="tabler-layout-dashboard" size="20" class="me-2" />
|
||||
Components Library
|
||||
<VIcon icon="tabler-chevron-up" size="16" class="ms-2" />
|
||||
</VBtn>
|
||||
|
||||
<!-- Components Library Panel -->
|
||||
<VCard
|
||||
v-if="isLibraryOpen"
|
||||
class="components-library-panel"
|
||||
:class="{ 'panel-open': isLibraryOpen }"
|
||||
elevation="8"
|
||||
>
|
||||
<VCardTitle class="library-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<VIcon icon="tabler-layout-dashboard" size="24" class="me-2" />
|
||||
<span>Dashboard Components</span>
|
||||
</div>
|
||||
<VBtn
|
||||
@click="toggleLibrary"
|
||||
variant="text"
|
||||
size="small"
|
||||
icon
|
||||
class="close-btn"
|
||||
>
|
||||
<VIcon icon="tabler-x" size="20" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText class="library-content">
|
||||
<div class="search-section">
|
||||
<VTextField
|
||||
v-model="searchQuery"
|
||||
placeholder="Search components..."
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="tabler-search"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="categories-tabs">
|
||||
<VChipGroup
|
||||
v-model="selectedCategory"
|
||||
selected-class="text-primary"
|
||||
class="category-chips"
|
||||
>
|
||||
<VChip
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:value="category"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{{ category }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</div>
|
||||
|
||||
<div class="components-grid">
|
||||
<div
|
||||
v-for="component in filteredComponents"
|
||||
:key="component.id"
|
||||
class="component-item"
|
||||
:draggable="isEditMode"
|
||||
@dragstart="handleDragStart($event, component)"
|
||||
@dragend="handleDragEnd"
|
||||
:class="{ 'draggable': isEditMode, 'disabled': !isEditMode }"
|
||||
>
|
||||
<div class="component-preview">
|
||||
<VIcon :icon="component.icon" size="32" :color="component.color" />
|
||||
<div v-if="isEditMode" class="component-overlay">
|
||||
<VIcon icon="tabler-plus" size="20" color="white" />
|
||||
</div>
|
||||
<div v-if="!isEditMode" class="disabled-overlay">
|
||||
<VIcon icon="tabler-lock" size="16" color="grey" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="component-info">
|
||||
<h4 class="component-title">{{ component.title }}</h4>
|
||||
<p class="component-description">{{ component.description }}</p>
|
||||
<div class="component-meta">
|
||||
<VChip
|
||||
:color="component.color"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="component-type-chip"
|
||||
>
|
||||
{{ component.category }}
|
||||
</VChip>
|
||||
<VChip
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
color="grey"
|
||||
class="size-chip"
|
||||
>
|
||||
{{ component.defaultSize.cols }}×{{ Math.round(component.defaultSize.height) }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredComponents.length === 0" class="no-results">
|
||||
<VIcon icon="tabler-layout-dashboard" size="48" color="grey-lighten-1" />
|
||||
<h3>No components found</h3>
|
||||
<p>Try adjusting your search or category filter</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditMode" class="edit-mode-notice">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="ma-2"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="tabler-info-circle" />
|
||||
</template>
|
||||
<div>
|
||||
<strong>Edit Mode Required</strong>
|
||||
<br>
|
||||
Enable edit mode to add components to your dashboard
|
||||
</div>
|
||||
</VAlert>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Drop Zone Overlay -->
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="drop-zone-overlay"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDrop"
|
||||
@dragleave="handleDragLeave"
|
||||
>
|
||||
<div class="drop-zone-content">
|
||||
<VIcon icon="tabler-download" size="64" color="primary" />
|
||||
<h2>Drop component here to add to dashboard</h2>
|
||||
<p>Release to add the component to your dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['add-component'])
|
||||
|
||||
const isLibraryOpen = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('All')
|
||||
|
||||
const categories = ['All', 'CRM', 'Ecommerce', 'Analytics', 'Reports', 'Activity']
|
||||
|
||||
const availableComponents = ref([
|
||||
{
|
||||
id: 'crm-active-project',
|
||||
title: 'Active Project',
|
||||
description: 'Shows current active project details',
|
||||
icon: 'tabler-folder-open',
|
||||
color: 'primary',
|
||||
category: 'CRM',
|
||||
component: 'CrmActiveProject',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-activity-timeline',
|
||||
title: 'Activity Timeline',
|
||||
description: 'Recent activities and updates timeline',
|
||||
icon: 'tabler-timeline',
|
||||
color: 'info',
|
||||
category: 'Activity',
|
||||
component: 'CrmActivityTimeline',
|
||||
defaultSize: { cols: 6, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-analytics-sales',
|
||||
title: 'Sales Analytics',
|
||||
description: 'Sales performance and analytics',
|
||||
icon: 'tabler-chart-line',
|
||||
color: 'success',
|
||||
category: 'Analytics',
|
||||
component: 'CrmAnalyticsSales',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-earning-reports',
|
||||
title: 'Earning Reports',
|
||||
description: 'Yearly earning overview and reports',
|
||||
icon: 'tabler-report-money',
|
||||
color: 'warning',
|
||||
category: 'Reports',
|
||||
component: 'CrmEarningReportsYearlyOverview',
|
||||
defaultSize: { cols: 8, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-project-status',
|
||||
title: 'Project Status',
|
||||
description: 'Current status of all projects',
|
||||
icon: 'tabler-checkup-list',
|
||||
color: 'deep-purple',
|
||||
category: 'CRM',
|
||||
component: 'CrmProjectStatus',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-recent-transactions',
|
||||
title: 'Recent Transactions',
|
||||
description: 'Latest financial transactions',
|
||||
icon: 'tabler-credit-card',
|
||||
color: 'teal',
|
||||
category: 'CRM',
|
||||
component: 'CrmRecentTransactions',
|
||||
defaultSize: { cols: 6, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'crm-sales-countries',
|
||||
title: 'Sales by Countries',
|
||||
description: 'Geographic sales distribution',
|
||||
icon: 'tabler-world',
|
||||
color: 'indigo',
|
||||
category: 'Analytics',
|
||||
component: 'CrmSalesByCountries',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'project-activity-bar',
|
||||
title: 'Project Activity Chart',
|
||||
description: 'Bar chart showing project activities',
|
||||
icon: 'tabler-chart-bar',
|
||||
color: 'cyan',
|
||||
category: 'Analytics',
|
||||
component: 'ProjectActivityBarChart',
|
||||
defaultSize: { cols: 8, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'analysis-card-1',
|
||||
title: 'Analysis Card (Projects)',
|
||||
description: 'Active projects progress analysis',
|
||||
icon: 'tabler-analytics',
|
||||
color: 'pink',
|
||||
category: 'Analytics',
|
||||
component: 'AnalysisCard',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: { chartName: 'Active Projects Progress' }
|
||||
},
|
||||
{
|
||||
id: 'analysis-card-2',
|
||||
title: 'Analysis Card (Cost)',
|
||||
description: 'Cost overview analysis',
|
||||
icon: 'tabler-analytics',
|
||||
color: 'pink',
|
||||
category: 'Analytics',
|
||||
component: 'AnalysisCard',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: { chartName: 'Cost overview' }
|
||||
},
|
||||
{
|
||||
id: 'cost-overview',
|
||||
title: 'Cost Overview',
|
||||
description: 'Detailed cost breakdown and overview',
|
||||
icon: 'tabler-coin',
|
||||
color: 'orange',
|
||||
category: 'Reports',
|
||||
component: 'CostOverview',
|
||||
defaultSize: { cols: 8, height: 33.33 },
|
||||
defaultProps: {}
|
||||
},
|
||||
{
|
||||
id: 'generated-leads-1',
|
||||
title: 'Generated Leads (Primary)',
|
||||
description: 'Lead generation progress - Primary theme',
|
||||
icon: 'tabler-users',
|
||||
color: 'primary',
|
||||
category: 'Ecommerce',
|
||||
component: 'GeneratedLeadsCard',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: { progress: 33 }
|
||||
},
|
||||
{
|
||||
id: 'generated-leads-2',
|
||||
title: 'Generated Leads (Success)',
|
||||
description: 'Lead generation progress - Success theme',
|
||||
icon: 'tabler-users',
|
||||
color: 'success',
|
||||
category: 'Ecommerce',
|
||||
component: 'GeneratedLeadsCard',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: { donutColors: ["primary"], progress: 71 }
|
||||
},
|
||||
{
|
||||
id: 'generated-leads-3',
|
||||
title: 'Generated Leads (Warning)',
|
||||
description: 'Lead generation progress - Warning theme',
|
||||
icon: 'tabler-users',
|
||||
color: 'warning',
|
||||
category: 'Ecommerce',
|
||||
component: 'GeneratedLeadsCard',
|
||||
defaultSize: { cols: 4, height: 33.33 },
|
||||
defaultProps: { donutColors: ["warning"], progress: 56 }
|
||||
}
|
||||
])
|
||||
|
||||
const filteredComponents = computed(() => {
|
||||
let components = availableComponents.value
|
||||
|
||||
// Filter by category
|
||||
if (selectedCategory.value !== 'All') {
|
||||
components = components.filter(comp => comp.category === selectedCategory.value)
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
components = components.filter(comp =>
|
||||
comp.title.toLowerCase().includes(query) ||
|
||||
comp.description.toLowerCase().includes(query) ||
|
||||
comp.category.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return components
|
||||
})
|
||||
|
||||
const toggleLibrary = () => {
|
||||
isLibraryOpen.value = !isLibraryOpen.value
|
||||
}
|
||||
|
||||
const handleDragStart = (event, component) => {
|
||||
if (!props.isEditMode) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
isDragging.value = true
|
||||
event.dataTransfer.setData('application/json', JSON.stringify(component))
|
||||
event.dataTransfer.effectAllowed = 'copy'
|
||||
|
||||
// Create drag image
|
||||
const dragImage = event.target.cloneNode(true)
|
||||
dragImage.style.transform = 'rotate(5deg) scale(0.8)'
|
||||
dragImage.style.opacity = '0.8'
|
||||
dragImage.style.zIndex = '9999'
|
||||
document.body.appendChild(dragImage)
|
||||
event.dataTransfer.setDragImage(dragImage, 50, 50)
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(dragImage)) {
|
||||
document.body.removeChild(dragImage)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
|
||||
try {
|
||||
const componentData = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
emit('add-component', componentData)
|
||||
|
||||
// Show success feedback
|
||||
console.log('Component added successfully:', componentData.title)
|
||||
} catch (error) {
|
||||
console.error('Error adding component:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
// Only hide if we're leaving the drop zone entirely
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
isDragging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Close library when clicking outside
|
||||
const handleClickOutside = (event) => {
|
||||
if (isLibraryOpen.value && !event.target.closest('.components-library-panel, .library-toggle-btn')) {
|
||||
isLibraryOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.components-library-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.library-toggle-btn {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(var(--v-theme-primary), 0.2) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 12px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: none !important;
|
||||
backdrop-filter: blur(10px);
|
||||
animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.library-toggle-btn:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 12px 40px rgba(var(--v-theme-primary), 0.3) !important;
|
||||
}
|
||||
|
||||
.components-library-panel {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
width: 450px;
|
||||
max-height: 600px;
|
||||
border-radius: 16px !important;
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.library-header {
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.1), rgba(var(--v-theme-secondary), 0.05));
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.library-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.categories-tabs {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.category-chips {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.components-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.component-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.component-item.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.component-item.draggable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
background: rgba(var(--v-theme-primary), 0.02);
|
||||
}
|
||||
|
||||
.component-item.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.component-item:active.draggable {
|
||||
cursor: grabbing;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(var(--v-theme-surface), 0.8);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.component-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(var(--v-theme-primary), 0.9);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.disabled-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.component-item.draggable:hover .component-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.component-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.component-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.25rem 0;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.component-description {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.component-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.component-type-chip,
|
||||
.size-chip {
|
||||
font-size: 0.7rem !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.no-results h3 {
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.no-results p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.edit-mode-notice {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.drop-zone-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(var(--v-theme-primary), 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.drop-zone-content {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
border: 3px dashed rgba(var(--v-theme-primary), 0.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
animation: dropZonePulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.drop-zone-content h2 {
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.drop-zone-content p {
|
||||
margin: 0;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3) translateY(100px);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropZonePulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
border-color: rgba(var(--v-theme-primary), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.components-library-container {
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.components-library-panel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.library-toggle-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.component-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.component-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.component-description {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
346
resources/js/components/CostOverview.vue
Normal file
346
resources/js/components/CostOverview.vue
Normal file
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<VCard
|
||||
class="overflow-visible"
|
||||
:style="cardBackgroundStyle"
|
||||
>
|
||||
<VCardText class="d-flex flex-column align-center pa-4">
|
||||
<!-- Header Section - Responsive -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-space-between align-start align-sm-center w-100 mb-4 gap-3">
|
||||
<div class="text-center text-sm-start">
|
||||
<h5 class="text-h5 text-wrap mb-2">
|
||||
Cost Overview
|
||||
</h5>
|
||||
<div class="text-body-1 mb-2 mb-sm-4 text-medium-emphasis">
|
||||
Monthly Breakdown
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-sm-end">
|
||||
<h3 class="text-h4 text-sm-h3 mb-1">
|
||||
{{ totalCostFormatted }}
|
||||
</h3>
|
||||
<div class="d-flex align-center justify-center justify-sm-end">
|
||||
<VIcon
|
||||
icon="tabler-trending-up"
|
||||
color="success"
|
||||
size="20"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-success font-weight-medium text-sm">
|
||||
+12.5%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Container - Responsive -->
|
||||
<div
|
||||
class="d-flex justify-center position-relative chart-container"
|
||||
>
|
||||
<VueApexCharts
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
type="donut"
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="chart-responsive"
|
||||
/>
|
||||
|
||||
<!-- Center Content -->
|
||||
<div class="chart-center-content">
|
||||
<div class="text-center">
|
||||
<h4 class="text-h5 text-sm-h4 mb-1">{{ totalCostFormatted }}</h4>
|
||||
<span class="text-body-2 text-medium-emphasis">Total Cost</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards - Responsive Grid -->
|
||||
<div class="stats-grid w-100 mt-4">
|
||||
<div
|
||||
v-for="(item, index) in costBreakdown"
|
||||
:key="index"
|
||||
class="stat-item pa-3 rounded-lg"
|
||||
:style="getItemCardStyle(index)"
|
||||
>
|
||||
<div class="d-flex align-center justify-center mb-2">
|
||||
<div
|
||||
class="dot me-2"
|
||||
:style="{ backgroundColor: chartColors[index] }"
|
||||
></div>
|
||||
<span class="text-body-2 text-medium-emphasis text-center">{{ item.label }}</span>
|
||||
</div>
|
||||
<h6 class="text-h6 text-center">{{ formatCurrency(item.value) }}</h6>
|
||||
<div class="text-caption text-center">{{ item.percentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
const currentTheme = computed(() => vuetifyTheme.current.value.colors)
|
||||
|
||||
const labels = ['Human Resources', 'Materials', 'Equipment']
|
||||
const series = ref([3000, 5000, 2000])
|
||||
|
||||
const totalCost = computed(() =>
|
||||
series.value.reduce((a, b) => a + b, 0)
|
||||
)
|
||||
|
||||
const totalCostFormatted = computed(() =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalCost.value)
|
||||
)
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
const costBreakdown = computed(() => {
|
||||
return labels.map((label, index) => ({
|
||||
label,
|
||||
value: series.value[index],
|
||||
percentage: Math.round((series.value[index] / totalCost.value) * 100)
|
||||
}))
|
||||
})
|
||||
|
||||
const chartColors = computed(() => [
|
||||
`rgba(${hexToRgb(currentTheme.value.primary)}, 1)`,
|
||||
`rgba(${hexToRgb(currentTheme.value.success)}, 1)`,
|
||||
`rgba(${hexToRgb(currentTheme.value.warning)}, 1)`
|
||||
])
|
||||
|
||||
// Card Background Style
|
||||
const cardBackgroundStyle = computed(() => {
|
||||
const primaryColor = currentTheme.value.primary
|
||||
|
||||
const createGradientColor = (color, opacity = 0.08) => {
|
||||
if (color.startsWith('#')) {
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
|
||||
const gradientColor1 = createGradientColor(primaryColor, 0.1)
|
||||
const gradientColor2 = createGradientColor(primaryColor, 0.03)
|
||||
|
||||
return {
|
||||
background: `linear-gradient(135deg,
|
||||
${gradientColor1} 0%,
|
||||
${gradientColor2} 50%,
|
||||
${gradientColor1} 100%)`
|
||||
}
|
||||
})
|
||||
|
||||
const getItemCardStyle = (index) => {
|
||||
const color = chartColors.value[index]
|
||||
const bgColor = color.replace('1)', '0.1)')
|
||||
|
||||
return {
|
||||
backgroundColor: bgColor,
|
||||
border: `1px solid ${color.replace('1)', '0.2)')}`
|
||||
}
|
||||
}
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
const labelColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])}, ${vuetifyTheme.current.value.variables['disabled-opacity']})`
|
||||
|
||||
return {
|
||||
labels,
|
||||
colors: chartColors.value,
|
||||
chart: {
|
||||
type: 'donut',
|
||||
toolbar: { show: false },
|
||||
sparkline: { enabled: false }
|
||||
},
|
||||
stroke: {
|
||||
colors: ['transparent'],
|
||||
width: 0
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
theme: vuetifyTheme.current.value.dark ? 'dark' : 'light',
|
||||
style: { fontFamily: 'inherit' },
|
||||
y: {
|
||||
formatter: (val) => formatCurrency(val)
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: (val) => Math.round(val) + '%',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
colors: ['#fff']
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '75%',
|
||||
labels: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
chart: {
|
||||
width: 280,
|
||||
height: 280
|
||||
},
|
||||
dataLabels: {
|
||||
style: {
|
||||
fontSize: '10px'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
chart: {
|
||||
width: 250,
|
||||
height: 250
|
||||
},
|
||||
dataLabels: {
|
||||
style: {
|
||||
fontSize: '9px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Chart Container - Responsive */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-width: 330px;
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.chart-container {
|
||||
max-width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.chart-container {
|
||||
max-width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-responsive {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.chart-center-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Stats Grid - Responsive */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) and (min-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
min-height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Responsive Typography */
|
||||
@media (max-width: 599px) {
|
||||
.text-h5 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.text-h4 {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.text-h5 {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
|
||||
.text-h4 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.chart-center-content h4 {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card padding adjustments for mobile */
|
||||
@media (max-width: 599px) {
|
||||
.pa-4 {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.pa-4 {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
resources/js/components/ErrorHeader.vue
Normal file
50
resources/js/components/ErrorHeader.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
statusCode: {
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
],
|
||||
required: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<!-- 👉 Title and subtitle -->
|
||||
<h1
|
||||
v-if="props.statusCode"
|
||||
class="header-title font-weight-medium mb-2"
|
||||
>
|
||||
{{ props.statusCode }}
|
||||
</h1>
|
||||
<h4
|
||||
v-if="props.title"
|
||||
class="text-h4 font-weight-medium mb-2"
|
||||
>
|
||||
{{ props.title }}
|
||||
</h4>
|
||||
<p
|
||||
v-if="props.description"
|
||||
class="text-body-1 mb-6"
|
||||
>
|
||||
{{ props.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-title {
|
||||
font-size: clamp(3rem, 5vw, 6rem);
|
||||
line-height: clamp(3rem, 5vw, 6rem);
|
||||
}
|
||||
</style>
|
||||
508
resources/js/components/ProjectActivityBarChart.vue
Normal file
508
resources/js/components/ProjectActivityBarChart.vue
Normal file
@@ -0,0 +1,508 @@
|
||||
<template>
|
||||
<VCard
|
||||
class="overflow-visible position-relative"
|
||||
:style="cardBackgroundStyle"
|
||||
elevation="8"
|
||||
>
|
||||
<!-- Decorative elements -->
|
||||
<div class="position-absolute decorative-circle-1"></div>
|
||||
<div class="position-absolute decorative-circle-2"></div>
|
||||
<div class="position-absolute decorative-dots"></div>
|
||||
|
||||
<VCardItem class="pb-4">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<VCardTitle class="text-h5 font-weight-bold mb-1">
|
||||
<VIcon icon="mdi-chart-line" class="me-2 text-primary"></VIcon>
|
||||
Active Projects Progress
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-subtitle-1 opacity-80">This Week Overview</VCardSubtitle>
|
||||
</div>
|
||||
<VChip
|
||||
:color="changePercent >= 0 ? 'success' : 'error'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="font-weight-bold"
|
||||
>
|
||||
{{ formattedChange }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pb-2">
|
||||
<!-- Progress Summary -->
|
||||
<div class="progress-summary-card mb-6">
|
||||
<div class="d-flex align-center justify-center mb-2">
|
||||
<div class="progress-circle">
|
||||
<h2 class="text-h2 font-weight-bold text-primary mb-0">{{ totalProgress }}%</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center text-subtitle-2 mb-0 opacity-80">Average Progress</p>
|
||||
</div>
|
||||
|
||||
<VueApexCharts
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
:height="330"
|
||||
width="100%"
|
||||
class="enhanced-chart"
|
||||
/>
|
||||
|
||||
<!-- Project Stats -->
|
||||
<!-- <div class="project-stats mt-4">
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<span class="text-subtitle-2 opacity-80">Projects Status</span>
|
||||
<VChip size="x-small" color="primary" variant="text">
|
||||
{{ values.length }} Total
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VChip
|
||||
v-for="(value, index) in values"
|
||||
:key="index"
|
||||
:color="getProjectStatusColor(value)"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
{{ labels[index] }}: {{ value }}%
|
||||
</VChip>
|
||||
</div>
|
||||
</div> -->
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const labels = [
|
||||
'Project Alpha',
|
||||
'Project Beta',
|
||||
'Project Gamma',
|
||||
'Project Delta',
|
||||
'Project Epsilon',
|
||||
]
|
||||
const values = [45, 70, 30, 85, 55]
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Progress Percentage',
|
||||
data: values,
|
||||
},
|
||||
]
|
||||
|
||||
const totalProgress = computed(() =>
|
||||
Math.round(values.reduce((sum, v) => sum + v, 0) / values.length)
|
||||
)
|
||||
|
||||
const changePercent = computed(() => {
|
||||
const last = values[values.length - 1]
|
||||
const prev = values[values.length - 2] ?? last
|
||||
return prev === 0 ? 0 : ((last - prev) / prev) * 100
|
||||
})
|
||||
|
||||
const formattedChange = computed(() => {
|
||||
const sign = changePercent.value >= 0 ? '+' : ''
|
||||
return `${sign}${changePercent.value.toFixed(1)}%`
|
||||
})
|
||||
|
||||
const getProjectStatusColor = (value) => {
|
||||
if (value >= 80) return 'success'
|
||||
if (value >= 60) return 'warning'
|
||||
if (value >= 40) return 'info'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
const cardBackgroundStyle = computed(() => {
|
||||
const currentTheme = vuetifyTheme.current.value.colors
|
||||
const primaryColor = currentTheme.primary
|
||||
|
||||
const createGradientColor = (color, opacity = 0.08) => {
|
||||
if (color.includes('rgba')) {
|
||||
return color.replace(/[\d\.]+\)$/g, `${opacity})`)
|
||||
}
|
||||
if (color.startsWith('#')) {
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
if (color.includes('rgb')) {
|
||||
return color.replace('rgb', 'rgba').replace(')', `, ${opacity})`)
|
||||
}
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
|
||||
const gradientColor1 = createGradientColor(primaryColor, 0.15)
|
||||
const gradientColor2 = createGradientColor(primaryColor, 0.02)
|
||||
const gradientColor3 = createGradientColor(primaryColor, 0.08)
|
||||
|
||||
return {
|
||||
background: `linear-gradient(135deg,
|
||||
${gradientColor1} 0%,
|
||||
${gradientColor2} 25%,
|
||||
${gradientColor3} 50%,
|
||||
${gradientColor2} 75%,
|
||||
${gradientColor1} 100%)`,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: `1px solid ${createGradientColor(primaryColor, 0.2)}`
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
chart: {
|
||||
type: 'bar',
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
background: 'transparent',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
animateGradually: {
|
||||
enabled: true,
|
||||
delay: 150
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 350
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "35%",
|
||||
borderRadius: 20,
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
dataLabels: {
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
},
|
||||
colors: ['rgba(var(--v-theme-primary), 1)'],
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: 'rgba(var(--v-theme-on-surface), 0.08)',
|
||||
strokeDashArray: 3,
|
||||
position: 'back',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
row: {
|
||||
colors: undefined,
|
||||
opacity: 0.5
|
||||
},
|
||||
column: {
|
||||
colors: undefined,
|
||||
opacity: 0.5
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 10,
|
||||
bottom: 0,
|
||||
left: 10
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetY: -20,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
colors: ['rgba(var(--v-theme-primary), 1)']
|
||||
},
|
||||
formatter: function (val) {
|
||||
return val + '%'
|
||||
}
|
||||
},
|
||||
legend: { show: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: 'dark',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Arial, sans-serif'
|
||||
},
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val + '%'
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: labels,
|
||||
axisBorder: {
|
||||
show: true,
|
||||
color: 'rgba(var(--v-theme-on-surface), 0.12)'
|
||||
},
|
||||
axisTicks: {
|
||||
show: true,
|
||||
color: 'rgba(var(--v-theme-on-surface), 0.12)'
|
||||
},
|
||||
labels: {
|
||||
show: true,
|
||||
rotate: -45,
|
||||
rotateAlways: false,
|
||||
hideOverlappingLabels: true,
|
||||
showDuplicates: false,
|
||||
trim: false,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
colors: 'rgba(var(--v-theme-on-surface), 0.7)',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
fontSize: '11px',
|
||||
colors: 'rgba(var(--v-theme-on-surface), 0.6)',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
formatter: function (val) {
|
||||
return val + '%'
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: 100
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1441,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '45%',
|
||||
borderRadius: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1368,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '40%',
|
||||
borderRadius: 16,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1264,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '35%',
|
||||
borderRadius: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 960,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '30%',
|
||||
borderRadius: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 883,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '35%',
|
||||
borderRadius: 16,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '40%',
|
||||
borderRadius: 14,
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
offsetY: -15
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '45%',
|
||||
borderRadius: 16,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 479,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 12,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
padding: { right: 5, left: 5, bottom: 10 },
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 400,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 10,
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
rotate: -90
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.decorative-circle-1 {
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.decorative-circle-2 {
|
||||
bottom: -30px;
|
||||
left: -30px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: radial-gradient(circle, rgba(var(--v-theme-secondary), 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.decorative-dots {
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
width: 6px;
|
||||
height: 40px;
|
||||
background-image: radial-gradient(circle, rgba(var(--v-theme-primary), 0.3) 1px, transparent 1px);
|
||||
background-size: 6px 8px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.progress-summary-card {
|
||||
background: rgba(var(--v-theme-surface), 0.6);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-summary-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(var(--v-theme-primary), 0.3) 0%,
|
||||
rgba(var(--v-theme-primary), 0.8) 50%,
|
||||
rgba(var(--v-theme-primary), 0.3) 100%);
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.progress-circle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 3px solid rgba(var(--v-theme-primary), 0.2);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
background: rgba(var(--v-theme-surface), 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.enhanced-chart {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.v-card:hover {
|
||||
transform: translateY(-2px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.v-card:hover .decorative-circle-1 {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.v-card:hover .decorative-circle-2 {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
106
resources/js/components/dialogs/AddAuthenticatorAppDialog.vue
Normal file
106
resources/js/components/dialogs/AddAuthenticatorAppDialog.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import themeselectionQr from '@images/pages/themeselection-qr.png'
|
||||
|
||||
const props = defineProps({
|
||||
authCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'submit',
|
||||
])
|
||||
|
||||
const authCode = ref(structuredClone(toRaw(props.authCode)))
|
||||
|
||||
const formSubmit = () => {
|
||||
if (authCode.value) {
|
||||
emit('submit', authCode.value)
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetAuthCode = () => {
|
||||
authCode.value = structuredClone(toRaw(props.authCode))
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-6">
|
||||
Add Authenticator App
|
||||
</h4>
|
||||
<h5 class="text-h5 mb-2">
|
||||
Authenticator Apps
|
||||
</h5>
|
||||
|
||||
<p class="text-body-1 mb-6">
|
||||
Using an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password, scan the QR code. It will generate a 6 digit code for you to enter below.
|
||||
</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<VImg
|
||||
width="150"
|
||||
:src="themeselectionQr"
|
||||
class="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VAlert
|
||||
title="ASDLKNASDA9AHS678dGhASD78AB"
|
||||
text="If you are unable to scan the QR code, you can manually enter the secret key below."
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
/>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<AppTextField
|
||||
v-model="authCode"
|
||||
name="auth-code"
|
||||
label="Enter Authentication Code"
|
||||
placeholder="123 456"
|
||||
class="mt-4 mb-6"
|
||||
/>
|
||||
|
||||
<div class="d-flex justify-end flex-wrap gap-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="resetAuthCode"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="formSubmit"
|
||||
>
|
||||
Continue
|
||||
<VIcon
|
||||
end
|
||||
icon="tabler-arrow-right"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
233
resources/js/components/dialogs/AddEditAddressDialog.vue
Normal file
233
resources/js/components/dialogs/AddEditAddressDialog.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<script setup>
|
||||
import home from '@images/svg/home.svg'
|
||||
import office from '@images/svg/office.svg'
|
||||
|
||||
const props = defineProps({
|
||||
billingAddress: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
selectedCountry: null,
|
||||
addressLine1: '',
|
||||
addressLine2: '',
|
||||
landmark: '',
|
||||
contact: '',
|
||||
country: null,
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: null,
|
||||
}),
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'submit',
|
||||
])
|
||||
|
||||
const billingAddress = ref(structuredClone(toRaw(props.billingAddress)))
|
||||
|
||||
const resetForm = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
billingAddress.value = structuredClone(toRaw(props.billingAddress))
|
||||
}
|
||||
|
||||
const onFormSubmit = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
emit('submit', billingAddress.value)
|
||||
}
|
||||
|
||||
const selectedAddress = ref('Home')
|
||||
|
||||
const addressTypes = [
|
||||
{
|
||||
icon: {
|
||||
icon: home,
|
||||
size: '28',
|
||||
},
|
||||
title: 'Home',
|
||||
desc: 'Delivery Time (9am - 9pm)',
|
||||
value: 'Home',
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
icon: office,
|
||||
size: '28',
|
||||
},
|
||||
title: 'Office',
|
||||
desc: 'Delivery Time (9am - 5pm)',
|
||||
value: 'Office',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900 "
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="val => $emit('update:isDialogVisible', val)"
|
||||
>
|
||||
<!-- 👉 Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard
|
||||
v-if="props.billingAddress"
|
||||
class="pa-sm-10 pa-2"
|
||||
>
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
{{ (props.billingAddress.addressLine1 || props.billingAddress.addressLine2) ? 'Edit' : 'Add New' }} Address
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Add new address for express delivery
|
||||
</p>
|
||||
|
||||
<div class="d-flex mb-6">
|
||||
<CustomRadiosWithIcon
|
||||
v-model:selected-radio="selectedAddress"
|
||||
:radio-content="addressTypes"
|
||||
:grid-column="{ sm: '6', cols: '12' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Form -->
|
||||
<VForm @submit.prevent="onFormSubmit">
|
||||
<VRow>
|
||||
<!-- 👉 First Name -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.firstName"
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Last Name -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.lastName"
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Select Country -->
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="billingAddress.selectedCountry"
|
||||
label="Select Country"
|
||||
placeholder="Select Country"
|
||||
:items="['USA', 'Aus', 'Canada', 'NZ']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Address Line 1 -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="billingAddress.addressLine1"
|
||||
label="Address Line 1"
|
||||
placeholder="12, Business Park"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Address Line 2 -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="billingAddress.addressLine2"
|
||||
label="Address Line 2"
|
||||
placeholder="Mall Road"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Landmark -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.landmark"
|
||||
label="Landmark"
|
||||
placeholder="Nr. Hard Rock Cafe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 City -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.city"
|
||||
label="City"
|
||||
placeholder="Los Angeles"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 State -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.state"
|
||||
label="State"
|
||||
placeholder="California"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Zip Code -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="billingAddress.zipCode"
|
||||
label="Zip Code"
|
||||
placeholder="99950"
|
||||
type="number"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSwitch label="Use as a billing address?" />
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Submit and Cancel button -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-3"
|
||||
>
|
||||
submit
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
@click="resetForm"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
95
resources/js/components/dialogs/AddEditPermissionDialog.vue
Normal file
95
resources/js/components/dialogs/AddEditPermissionDialog.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
permissionName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'update:permissionName',
|
||||
])
|
||||
|
||||
const currentPermissionName = ref('')
|
||||
|
||||
const onReset = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
currentPermissionName.value = ''
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
emit('update:permissionName', currentPermissionName.value)
|
||||
}
|
||||
|
||||
watch(() => props, () => {
|
||||
currentPermissionName.value = props.permissionName
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 600"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="onReset"
|
||||
>
|
||||
<!-- 👉 dialog close btn -->
|
||||
<DialogCloseBtn @click="onReset" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
{{ props.permissionName ? 'Edit' : 'Add' }} Permission
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
{{ props.permissionName ? 'Edit' : 'Add' }} permission as per your requirements.
|
||||
</p>
|
||||
|
||||
<!-- 👉 Form -->
|
||||
<VForm>
|
||||
<VAlert
|
||||
type="warning"
|
||||
title="Warning!"
|
||||
variant="tonal"
|
||||
class="mb-6"
|
||||
>
|
||||
<template #text>
|
||||
By {{ props.permissionName ? 'editing' : 'adding' }} the permission name, you might break the system permissions functionality.
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<!-- 👉 Role name -->
|
||||
<div class="d-flex gap-4 mb-6 flex-wrap flex-column flex-sm-row">
|
||||
<AppTextField
|
||||
v-model="currentPermissionName"
|
||||
placeholder="Enter Permission Name"
|
||||
/>
|
||||
|
||||
<VBtn @click="onSubmit">
|
||||
{{ props.permissionName ? 'Update' : 'Add' }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VCheckbox label="Set as core permission" />
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.permission-table {
|
||||
td {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
282
resources/js/components/dialogs/AddEditRoleDialog.vue
Normal file
282
resources/js/components/dialogs/AddEditRoleDialog.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<script setup>
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
|
||||
const props = defineProps({
|
||||
rolePermissions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
name: '',
|
||||
permissions: [],
|
||||
}),
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'update:rolePermissions',
|
||||
])
|
||||
|
||||
const permissions = ref([
|
||||
{
|
||||
name: 'User Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Content Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Disputes Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Database Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Financial Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Reporting',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'API Control',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Repository Management',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
{
|
||||
name: 'Payroll',
|
||||
read: false,
|
||||
write: false,
|
||||
create: false,
|
||||
},
|
||||
])
|
||||
|
||||
const isSelectAll = ref(false)
|
||||
const role = ref('')
|
||||
const refPermissionForm = ref()
|
||||
|
||||
const checkedCount = computed(() => {
|
||||
let counter = 0
|
||||
permissions.value.forEach(permission => {
|
||||
Object.entries(permission).forEach(([key, value]) => {
|
||||
if (key !== 'name' && value)
|
||||
counter++
|
||||
})
|
||||
})
|
||||
|
||||
return counter
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => checkedCount.value > 0 && checkedCount.value < permissions.value.length * 3)
|
||||
|
||||
watch(isSelectAll, val => {
|
||||
permissions.value = permissions.value.map(permission => ({
|
||||
...permission,
|
||||
read: val,
|
||||
write: val,
|
||||
create: val,
|
||||
}))
|
||||
})
|
||||
watch(isIndeterminate, () => {
|
||||
if (!isIndeterminate.value)
|
||||
isSelectAll.value = false
|
||||
})
|
||||
watch(permissions, () => {
|
||||
if (checkedCount.value === permissions.value.length * 3)
|
||||
isSelectAll.value = true
|
||||
}, { deep: true })
|
||||
watch(() => props, () => {
|
||||
if (props.rolePermissions && props.rolePermissions.permissions.length) {
|
||||
role.value = props.rolePermissions.name
|
||||
permissions.value = permissions.value.map(permission => {
|
||||
const rolePermission = props.rolePermissions?.permissions.find(item => item.name === permission.name)
|
||||
if (rolePermission) {
|
||||
return {
|
||||
...permission,
|
||||
...rolePermission,
|
||||
}
|
||||
}
|
||||
|
||||
return permission
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = () => {
|
||||
const rolePermissions = {
|
||||
name: role.value,
|
||||
permissions: permissions.value,
|
||||
}
|
||||
|
||||
emit('update:rolePermissions', rolePermissions)
|
||||
emit('update:isDialogVisible', false)
|
||||
isSelectAll.value = false
|
||||
refPermissionForm.value?.reset()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
isSelectAll.value = false
|
||||
refPermissionForm.value?.reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="onReset"
|
||||
>
|
||||
<!-- 👉 Dialog close btn -->
|
||||
<DialogCloseBtn @click="onReset" />
|
||||
|
||||
<VCard class="pa-sm-10 pa-2">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
{{ props.rolePermissions.name ? 'Edit' : 'Add New' }} Role
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Set Role Permissions
|
||||
</p>
|
||||
|
||||
<!-- 👉 Form -->
|
||||
<VForm ref="refPermissionForm">
|
||||
<!-- 👉 Role name -->
|
||||
<AppTextField
|
||||
v-model="role"
|
||||
label="Role Name"
|
||||
placeholder="Enter Role Name"
|
||||
/>
|
||||
|
||||
<h5 class="text-h5 my-6">
|
||||
Role Permissions
|
||||
</h5>
|
||||
|
||||
<!-- 👉 Role Permissions -->
|
||||
|
||||
<VTable class="permission-table text-no-wrap mb-6">
|
||||
<!-- 👉 Admin -->
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6">
|
||||
Administrator Access
|
||||
</h6>
|
||||
</td>
|
||||
<td colspan="3">
|
||||
<div class="d-flex justify-end">
|
||||
<VCheckbox
|
||||
v-model="isSelectAll"
|
||||
v-model:indeterminate="isIndeterminate"
|
||||
label="Select All"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 👉 Other permission loop -->
|
||||
<template
|
||||
v-for="permission in permissions"
|
||||
:key="permission.name"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 class="text-h6">
|
||||
{{ permission.name }}
|
||||
</h6>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-end">
|
||||
<VCheckbox
|
||||
v-model="permission.read"
|
||||
label="Read"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-end">
|
||||
<VCheckbox
|
||||
v-model="permission.write"
|
||||
label="Write"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-end">
|
||||
<VCheckbox
|
||||
v-model="permission.create"
|
||||
label="Create"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</VTable>
|
||||
|
||||
<!-- 👉 Actions button -->
|
||||
<div class="d-flex align-center justify-center gap-4">
|
||||
<VBtn @click="onSubmit">
|
||||
Submit
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="onReset"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.permission-table {
|
||||
td {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
padding-block: 0.5rem;
|
||||
|
||||
.v-checkbox {
|
||||
min-inline-size: 4.75rem;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.v-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
resources/js/components/dialogs/AddPaymentMethodDialog.vue
Normal file
117
resources/js/components/dialogs/AddPaymentMethodDialog.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import americanExDark from '@images/icons/payments/img/ae-dark.png'
|
||||
import americanExLight from '@images/icons/payments/img/american-express.png'
|
||||
import dcDark from '@images/icons/payments/img/dc-dark.png'
|
||||
import dcLight from '@images/icons/payments/img/dc-light.png'
|
||||
import jcbDark from '@images/icons/payments/img/jcb-dark.png'
|
||||
import jcbLight from '@images/icons/payments/img/jcb-light.png'
|
||||
import masterCardDark from '@images/icons/payments/img/master-dark.png'
|
||||
import masterCardLight from '@images/icons/payments/img/mastercard.png'
|
||||
import visaDark from '@images/icons/payments/img/visa-dark.png'
|
||||
import visaLight from '@images/icons/payments/img/visa-light.png'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const visa = useGenerateImageVariant(visaLight, visaDark)
|
||||
const masterCard = useGenerateImageVariant(masterCardLight, masterCardDark)
|
||||
const americanEx = useGenerateImageVariant(americanExLight, americanExDark)
|
||||
const jcb = useGenerateImageVariant(jcbLight, jcbDark)
|
||||
const dc = useGenerateImageVariant(dcLight, dcDark)
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
const paymentMethodsData = [
|
||||
{
|
||||
title: 'Visa',
|
||||
type: 'Credit Card',
|
||||
img: visa,
|
||||
},
|
||||
{
|
||||
title: 'American Express',
|
||||
type: 'Credit Card',
|
||||
img: americanEx,
|
||||
},
|
||||
{
|
||||
title: 'Mastercard',
|
||||
type: 'Credit Card',
|
||||
img: masterCard,
|
||||
},
|
||||
{
|
||||
title: 'JCB',
|
||||
type: 'Credit Card',
|
||||
img: jcb,
|
||||
},
|
||||
{
|
||||
title: 'Diners Club',
|
||||
type: 'Credit Card',
|
||||
img: dc,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 750"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<!-- 👉 dialog close btn -->
|
||||
<DialogCloseBtn @click="emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Add payment methods
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Supported payment methods
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in paymentMethodsData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center py-4 gap-x-4">
|
||||
<div class="d-flex align-center">
|
||||
<VImg
|
||||
:src="item.img.value"
|
||||
height="30"
|
||||
width="50"
|
||||
class="me-4"
|
||||
/>
|
||||
<h6 class="text-h6">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-none d-sm-block text-body-1">
|
||||
{{ item.type }}
|
||||
</div>
|
||||
</div>
|
||||
<VDivider v-if="index !== paymentMethodsData.length - 1" />
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.refer-link-input {
|
||||
.v-field--appended {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.v-field__append-inner {
|
||||
padding-block-start: 0.125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
146
resources/js/components/dialogs/CardAddEditDialog.vue
Normal file
146
resources/js/components/dialogs/CardAddEditDialog.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
cardDetails: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
number: '',
|
||||
name: '',
|
||||
expiry: '',
|
||||
cvv: '',
|
||||
isPrimary: false,
|
||||
type: '',
|
||||
}),
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'update:isDialogVisible',
|
||||
])
|
||||
|
||||
const cardDetails = ref(structuredClone(toRaw(props.cardDetails)))
|
||||
|
||||
watch(() => props, () => {
|
||||
cardDetails.value = structuredClone(toRaw(props.cardDetails))
|
||||
})
|
||||
|
||||
const formSubmit = () => {
|
||||
emit('submit', cardDetails.value)
|
||||
}
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 600"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<!-- 👉 Title -->
|
||||
<VCardItem class="text-center">
|
||||
<VCardTitle>
|
||||
<h4 class="text-h4 mb-2">
|
||||
{{ props.cardDetails.name ? 'Edit Card' : 'Add New Card' }}
|
||||
</h4>
|
||||
</VCardTitle>
|
||||
<p class="text-body-1 mb-0">
|
||||
{{ props.cardDetails.name ? 'Edit your saved card details' : 'Add card for future billing' }}
|
||||
</p>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-6">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<!-- 👉 Card Number -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="cardDetails.number"
|
||||
label="Card Number"
|
||||
placeholder="1356 3215 6548 7898"
|
||||
type="number"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Card Name -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="cardDetails.name"
|
||||
label="Name"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Card Expiry -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="cardDetails.expiry"
|
||||
label="Expiry Date"
|
||||
placeholder="MM/YY"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Card CVV -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="cardDetails.cvv"
|
||||
type="number"
|
||||
label="CVV Code"
|
||||
placeholder="654"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Card Primary Set -->
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="cardDetails.isPrimary"
|
||||
label="Save Card for future billing?"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Card actions -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<VBtn
|
||||
class="me-4"
|
||||
type="submit"
|
||||
@click="formSubmit"
|
||||
>
|
||||
Submit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="$emit('update:isDialogVisible', false)"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
165
resources/js/components/dialogs/ConfirmDialog.vue
Normal file
165
resources/js/components/dialogs/ConfirmDialog.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
confirmationQuestion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
confirmTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
confirmMsg: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cancelTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cancelMsg: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'confirm',
|
||||
])
|
||||
|
||||
const unsubscribed = ref(false)
|
||||
const cancelled = ref(false)
|
||||
|
||||
const updateModelValue = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
const onConfirmation = () => {
|
||||
emit('confirm', true)
|
||||
updateModelValue(false)
|
||||
unsubscribed.value = true
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
emit('confirm', false)
|
||||
emit('update:isDialogVisible', false)
|
||||
cancelled.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Confirm Dialog -->
|
||||
<VDialog
|
||||
max-width="500"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="updateModelValue"
|
||||
>
|
||||
<VCard class="text-center px-10 py-6">
|
||||
<VCardText>
|
||||
<VBtn
|
||||
icon
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
class="my-4"
|
||||
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
|
||||
>
|
||||
<span class="text-5xl">!</span>
|
||||
</VBtn>
|
||||
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
{{ props.confirmationQuestion }}
|
||||
</h6>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex align-center justify-center gap-2">
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
@click="onConfirmation"
|
||||
>
|
||||
Confirm
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="onCancel"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Unsubscribed -->
|
||||
<VDialog
|
||||
v-model="unsubscribed"
|
||||
max-width="500"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="text-center px-10 py-6">
|
||||
<VBtn
|
||||
icon
|
||||
variant="outlined"
|
||||
color="success"
|
||||
class="my-4"
|
||||
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-check"
|
||||
size="38"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<h1 class="text-h4 mb-4">
|
||||
{{ props.confirmTitle }}
|
||||
</h1>
|
||||
|
||||
<p>{{ props.confirmMsg }}</p>
|
||||
|
||||
<VBtn
|
||||
color="success"
|
||||
@click="unsubscribed = false"
|
||||
>
|
||||
Ok
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Cancelled -->
|
||||
<VDialog
|
||||
v-model="cancelled"
|
||||
max-width="500"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="text-center px-10 py-6">
|
||||
<VBtn
|
||||
icon
|
||||
variant="outlined"
|
||||
color="error"
|
||||
class="my-4"
|
||||
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
|
||||
>
|
||||
<span class="text-5xl font-weight-light">X</span>
|
||||
</VBtn>
|
||||
|
||||
<h1 class="text-h4 mb-4">
|
||||
{{ props.cancelTitle }}
|
||||
</h1>
|
||||
|
||||
<p>{{ props.cancelMsg }}</p>
|
||||
|
||||
<VBtn
|
||||
color="success"
|
||||
@click="cancelled = false"
|
||||
>
|
||||
Ok
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
463
resources/js/components/dialogs/CreateAppDialog.vue
Normal file
463
resources/js/components/dialogs/CreateAppDialog.vue
Normal file
@@ -0,0 +1,463 @@
|
||||
<script setup>
|
||||
import laptopGirl from '@images/illustrations/laptop-girl.png'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'updatedData',
|
||||
])
|
||||
|
||||
const currentStep = ref(0)
|
||||
|
||||
const createApp = [
|
||||
{
|
||||
icon: 'tabler-file-text',
|
||||
title: 'DETAILS',
|
||||
subtitle: 'Enter Details',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-id',
|
||||
title: 'FRAMEWORKS',
|
||||
subtitle: 'Select Framework',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-database',
|
||||
title: 'DATABASE',
|
||||
subtitle: 'Select Database',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-credit-card',
|
||||
title: 'BILLING',
|
||||
subtitle: 'Payment Details',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-check',
|
||||
title: 'SUBMIT',
|
||||
subtitle: 'Submit',
|
||||
},
|
||||
]
|
||||
|
||||
const categories = [
|
||||
{
|
||||
icon: 'tabler-file-text',
|
||||
color: 'info',
|
||||
title: 'CRM Application',
|
||||
subtitle: 'Scales with any business',
|
||||
slug: 'crm-application',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-shopping-cart',
|
||||
color: 'success',
|
||||
title: 'Ecommerce Platforms',
|
||||
subtitle: 'Grow Your Business With App',
|
||||
slug: 'ecommerce-application',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-device-laptop',
|
||||
color: 'error',
|
||||
title: 'Online Learning platform',
|
||||
subtitle: 'Start learning today',
|
||||
slug: 'online-learning-application',
|
||||
},
|
||||
]
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
icon: 'tabler-brand-react-native',
|
||||
color: 'info',
|
||||
title: 'React Native',
|
||||
subtitle: 'Create truly native apps',
|
||||
slug: 'react-framework',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-brand-angular',
|
||||
color: 'error',
|
||||
title: 'Angular',
|
||||
subtitle: 'Most suited for your application',
|
||||
slug: 'angular-framework',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-brand-vue',
|
||||
color: 'success',
|
||||
title: 'Vue',
|
||||
subtitle: 'JS web frameworks',
|
||||
slug: 'js-framework',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-brand-html5',
|
||||
color: 'warning',
|
||||
title: 'HTML',
|
||||
subtitle: 'Progressive Framework',
|
||||
slug: 'html-framework',
|
||||
},
|
||||
]
|
||||
|
||||
const databases = [
|
||||
{
|
||||
icon: 'tabler-brand-firebase',
|
||||
color: 'error',
|
||||
title: 'Firebase',
|
||||
subtitle: 'Cloud Firestore',
|
||||
slug: 'firebase-database',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-brand-amazon',
|
||||
color: 'warning',
|
||||
title: 'AWS',
|
||||
subtitle: 'Amazon Fast NoSQL Database',
|
||||
slug: 'aws-database',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-database',
|
||||
color: 'info',
|
||||
title: 'MySQL',
|
||||
subtitle: 'Basic MySQL database',
|
||||
slug: 'mysql-database',
|
||||
},
|
||||
]
|
||||
|
||||
const createAppData = ref({
|
||||
category: 'crm-application',
|
||||
framework: 'js-framework',
|
||||
database: 'firebase-database',
|
||||
cardNumber: null,
|
||||
cardName: '',
|
||||
cardExpiry: '',
|
||||
cardCvv: '',
|
||||
isSave: true,
|
||||
})
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
watch(() => props, () => {
|
||||
if (!props.isDialogVisible)
|
||||
currentStep.value = 0
|
||||
})
|
||||
|
||||
const onSubmit = () => {
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('submitted...!!')
|
||||
emit('updatedData', createAppData.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
max-width="900"
|
||||
min-height="590"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<!-- 👉 dialog close btn -->
|
||||
<DialogCloseBtn
|
||||
size="small"
|
||||
@click="emit('update:isDialogVisible', false)"
|
||||
/>
|
||||
<VCard
|
||||
class="create-app-dialog"
|
||||
min-height="590"
|
||||
>
|
||||
<VCardText class="pa-5 pa-sm-16">
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Create App
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Provide data with this form to create your app.
|
||||
</p>
|
||||
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="5"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<AppStepper
|
||||
v-model:current-step="currentStep"
|
||||
direction="vertical"
|
||||
:items="createApp"
|
||||
icon-size="22"
|
||||
class="stepper-icon-step-bg"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="7"
|
||||
md="8"
|
||||
lg="9"
|
||||
>
|
||||
<VWindow
|
||||
v-model="currentStep"
|
||||
class="disable-tab-transition stepper-content"
|
||||
>
|
||||
<!-- 👉 category -->
|
||||
<VWindowItem>
|
||||
<AppTextField
|
||||
label="Application Name"
|
||||
placeholder="Application Name"
|
||||
/>
|
||||
|
||||
<h5 class="text-h5 mt-6 mb-4">
|
||||
Category
|
||||
</h5>
|
||||
<VRadioGroup v-model="createAppData.category">
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="category in categories"
|
||||
:key="category.title"
|
||||
@click="createAppData.category = category.slug"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="46"
|
||||
rounded
|
||||
variant="tonal"
|
||||
:color="category.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="category.icon"
|
||||
size="30"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ category.title }}
|
||||
</h6>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ category.subtitle }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VRadio :value="category.slug" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VRadioGroup>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 👉 Frameworks -->
|
||||
<VWindowItem>
|
||||
<h5 class="text-h5 mb-4">
|
||||
Select Framework
|
||||
</h5>
|
||||
<VRadioGroup v-model="createAppData.framework">
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="framework in frameworks"
|
||||
:key="framework.title"
|
||||
@click="createAppData.framework = framework.slug"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="46"
|
||||
rounded
|
||||
variant="tonal"
|
||||
:color="framework.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="framework.icon"
|
||||
size="30"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ framework.title }}
|
||||
</h6>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ framework.subtitle }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VRadio :value="framework.slug" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VRadioGroup>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 👉 Database Engine -->
|
||||
<VWindowItem>
|
||||
<AppTextField
|
||||
label="Database Name"
|
||||
placeholder="UserDB"
|
||||
/>
|
||||
|
||||
<h5 class="text-h5 mt-6 mb-4">
|
||||
Select Database Engine
|
||||
</h5>
|
||||
<VRadioGroup v-model="createAppData.database">
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="database in databases"
|
||||
:key="database.title"
|
||||
@click="createAppData.database = database.slug"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
size="46"
|
||||
rounded
|
||||
variant="tonal"
|
||||
:color="database.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="database.icon"
|
||||
size="30"
|
||||
/>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<h6 class="text-h6 mb-1">
|
||||
{{ database.title }}
|
||||
</h6>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ database.subtitle }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VRadio :value="database.slug" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VRadioGroup>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 👉 Billing form -->
|
||||
<VWindowItem>
|
||||
<h6 class="text-h6 mb-6">
|
||||
Payment Details
|
||||
</h6>
|
||||
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="createAppData.cardNumber"
|
||||
label="Card Number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
type="number"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="createAppData.cardName"
|
||||
label="Name on Card"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="6"
|
||||
md="3"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="createAppData.cardExpiry"
|
||||
label="Expiry"
|
||||
placeholder="MM/YY"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="6"
|
||||
md="3"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="createAppData.cardCvv"
|
||||
label="CVV"
|
||||
placeholder="123"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="createAppData.isSave"
|
||||
label="Save Card for future billing?"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem class="text-center">
|
||||
<h5 class="text-h5 mb-1">
|
||||
Submit
|
||||
</h5>
|
||||
<p class="text-sm mb-4">
|
||||
Submit to kickstart your project.
|
||||
</p>
|
||||
|
||||
<VImg
|
||||
:src="laptopGirl"
|
||||
width="176"
|
||||
class="mx-auto"
|
||||
/>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
|
||||
<div class="d-flex justify-space-between mt-6">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
:disabled="currentStep === 0"
|
||||
@click="currentStep--"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-arrow-left"
|
||||
start
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
Previous
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="createApp.length - 1 === currentStep"
|
||||
color="success"
|
||||
@click="onSubmit"
|
||||
>
|
||||
submit
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-else
|
||||
@click="currentStep++"
|
||||
>
|
||||
Next
|
||||
|
||||
<VIcon
|
||||
icon="tabler-arrow-right"
|
||||
end
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.stepper-content .card-list {
|
||||
--v-card-list-gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
mobileNumber: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'submit',
|
||||
])
|
||||
|
||||
const phoneNumber = ref(structuredClone(toRaw(props.mobileNumber)))
|
||||
|
||||
const formSubmit = () => {
|
||||
if (phoneNumber.value) {
|
||||
emit('submit', phoneNumber.value)
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetPhoneNumber = () => {
|
||||
phoneNumber.value = structuredClone(toRaw(props.mobileNumber))
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h5 class="text-h5 mb-2">
|
||||
Verify Your Mobile Number for SMS
|
||||
</h5>
|
||||
<p class="text-body-1 mb-6">
|
||||
Enter your mobile phone number with country code and we will send you a verification code.
|
||||
</p>
|
||||
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<AppTextField
|
||||
v-model="phoneNumber"
|
||||
name="mobile"
|
||||
label="Phone Number"
|
||||
placeholder="+1 123 456 7890"
|
||||
type="number"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-wrap justify-end gap-4">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="resetPhoneNumber"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="formSubmit"
|
||||
>
|
||||
continue
|
||||
<VIcon
|
||||
end
|
||||
icon="tabler-arrow-right"
|
||||
class="flip-in-rtl"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
159
resources/js/components/dialogs/PaymentProvidersDialog.vue
Normal file
159
resources/js/components/dialogs/PaymentProvidersDialog.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup>
|
||||
import americanExDark from '@images/icons/payments/img/ae-dark.png'
|
||||
import americanExLight from '@images/icons/payments/img/american-express.png'
|
||||
import dcDark from '@images/icons/payments/img/dc-dark.png'
|
||||
import dcLight from '@images/icons/payments/img/dc-light.png'
|
||||
import jcbDark from '@images/icons/payments/img/jcb-dark.png'
|
||||
import jcbLight from '@images/icons/payments/img/jcb-light.png'
|
||||
import masterCardDark from '@images/icons/payments/img/master-dark.png'
|
||||
import masterCardLight from '@images/icons/payments/img/mastercard.png'
|
||||
import visaDark from '@images/icons/payments/img/visa-dark.png'
|
||||
import visaLight from '@images/icons/payments/img/visa-light.png'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const visa = useGenerateImageVariant(visaLight, visaDark)
|
||||
const masterCard = useGenerateImageVariant(masterCardLight, masterCardDark)
|
||||
const americanEx = useGenerateImageVariant(americanExLight, americanExDark)
|
||||
const jcb = useGenerateImageVariant(jcbLight, jcbDark)
|
||||
const dc = useGenerateImageVariant(dcLight, dcDark)
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
const paymentProvidersData = [
|
||||
{
|
||||
title: 'Adyen',
|
||||
providers: [
|
||||
visa,
|
||||
masterCard,
|
||||
americanEx,
|
||||
jcb,
|
||||
dc,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '2Checkout',
|
||||
providers: [
|
||||
visa,
|
||||
americanEx,
|
||||
jcb,
|
||||
dc,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Airpay',
|
||||
providers: [
|
||||
visa,
|
||||
americanEx,
|
||||
masterCard,
|
||||
jcb,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authorize.net',
|
||||
providers: [
|
||||
americanEx,
|
||||
jcb,
|
||||
dc,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Bambora',
|
||||
providers: [
|
||||
masterCard,
|
||||
americanEx,
|
||||
jcb,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Bambora',
|
||||
providers: [
|
||||
visa,
|
||||
masterCard,
|
||||
americanEx,
|
||||
jcb,
|
||||
dc,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Chase Paymentech (Orbital)',
|
||||
providers: [
|
||||
visa,
|
||||
americanEx,
|
||||
jcb,
|
||||
dc,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Checkout.com',
|
||||
providers: [
|
||||
visa,
|
||||
masterCard,
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Select Payment Providers
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Third-party payment providers
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in paymentProvidersData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="d-flex flex-column flex-sm-row justify-space-between gap-4 flex-wrap py-4">
|
||||
<h6 class="text-h6">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
<div class="d-flex gap-4 flex-wrap">
|
||||
<img
|
||||
v-for="(img, iterator) in item.providers"
|
||||
:key="iterator"
|
||||
:src="img.value"
|
||||
height="30"
|
||||
width="50"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<VDivider v-if="index !== paymentProvidersData.length - 1" />
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.refer-link-input {
|
||||
.v-field--appended {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.v-field__append-inner {
|
||||
padding-block-start: 0.125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
resources/js/components/dialogs/PricingPlanDialog.vue
Normal file
31
resources/js/components/dialogs/PricingPlanDialog.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 1200"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<!-- 👉 Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pricing-dialog pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<AppPricing md="4" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
178
resources/js/components/dialogs/ReferAndEarnDialog.vue
Normal file
178
resources/js/components/dialogs/ReferAndEarnDialog.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup>
|
||||
import keyboard from '@images/svg/keyboard.svg'
|
||||
import paper from '@images/svg/paper-send.svg'
|
||||
import rocket from '@images/svg/rocket.svg'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
const referAndEarnSteps = [
|
||||
{
|
||||
icon: paper,
|
||||
title: 'Send Invitation 👍🏻',
|
||||
subtitle: 'Send your referral link to your friend',
|
||||
},
|
||||
{
|
||||
icon: keyboard,
|
||||
title: 'Registration 😎',
|
||||
subtitle: 'Let them register to our services',
|
||||
},
|
||||
{
|
||||
icon: rocket,
|
||||
title: 'Free Trial 🎉',
|
||||
subtitle: 'Your friend will get 30 days free trial',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 800"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<!-- 👉 Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Refer & Earn
|
||||
</h4>
|
||||
<p class="text-body-1 mb-6 text-center">
|
||||
Invite your friend to <span class="text-capitalize">{{ themeConfig.app.title }}</span>, if they sign up, you and your friend will get 30 days free trial
|
||||
</p>
|
||||
|
||||
<VRow class="text-center mt-8">
|
||||
<VCol
|
||||
v-for="step in referAndEarnSteps"
|
||||
:key="step.title"
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VAvatar
|
||||
variant="tonal"
|
||||
size="88"
|
||||
color="primary"
|
||||
rounded
|
||||
>
|
||||
<VIcon
|
||||
size="40"
|
||||
:icon="step.icon"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<h5 class="text-h5 mt-4 mb-2">
|
||||
{{ step.title }}
|
||||
</h5>
|
||||
<div>{{ step.subtitle }}</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="mt-12 mb-6" />
|
||||
|
||||
<h5 class="text-h5 mb-6">
|
||||
Invite your friends
|
||||
</h5>
|
||||
|
||||
<VForm
|
||||
class="d-flex align-center flex-wrap gap-4"
|
||||
@submit.prevent="() => {}"
|
||||
>
|
||||
<AppTextField
|
||||
placeholder="johnDoe@gmail.com"
|
||||
label="Enter your friend's email address and invite them to join Vuexy 😍"
|
||||
/>
|
||||
|
||||
<VBtn
|
||||
class="align-self-end"
|
||||
type="submit"
|
||||
>
|
||||
Send
|
||||
</VBtn>
|
||||
</VForm>
|
||||
|
||||
<h5 class="text-h5 my-6">
|
||||
Share the referral link
|
||||
</h5>
|
||||
|
||||
<VForm
|
||||
class="d-flex align-center flex-wrap gap-4"
|
||||
@submit.prevent="() => {}"
|
||||
>
|
||||
<AppTextField
|
||||
placeholder="http://pixinvent.link"
|
||||
label="You can also copy and send it or share it on your social media. 🚀"
|
||||
class="refer-link-input"
|
||||
>
|
||||
<template #append-inner>
|
||||
<VBtn variant="text">
|
||||
Copy link
|
||||
</VBtn>
|
||||
</template>
|
||||
</AppTextField>
|
||||
|
||||
<div class="d-flex align-self-end gap-1">
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#3B5998"
|
||||
size="38"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="tabler-brand-facebook"
|
||||
size="22"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#55ACEE"
|
||||
size="38"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="tabler-brand-twitter"
|
||||
size="22"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
class="rounded"
|
||||
color="#007BB6"
|
||||
size="38"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="tabler-brand-linkedin"
|
||||
size="22"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.refer-link-input {
|
||||
.v-field--appended {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
resources/js/components/dialogs/ShareProjectDialog.vue
Normal file
186
resources/js/components/dialogs/ShareProjectDialog.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<script setup>
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar7 from '@images/avatars/avatar-7.png'
|
||||
import avatar8 from '@images/avatars/avatar-8.png'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const dialogVisibleUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
|
||||
const membersList = [
|
||||
{
|
||||
avatar: avatar1,
|
||||
name: 'Lester Palmer',
|
||||
email: 'jerrod98@gmail.com',
|
||||
permission: 'Can Edit',
|
||||
},
|
||||
{
|
||||
avatar: avatar2,
|
||||
name: 'Mattie Blair',
|
||||
email: 'prudence.boehm@yahoo.com',
|
||||
permission: 'Owner',
|
||||
},
|
||||
{
|
||||
avatar: avatar3,
|
||||
name: 'Marvin Wheeler',
|
||||
email: 'rumet@jujpejah.net',
|
||||
permission: 'Can Comment',
|
||||
},
|
||||
{
|
||||
avatar: avatar4,
|
||||
name: 'Nannie Ford',
|
||||
email: 'negza@nuv.io',
|
||||
permission: 'Can View',
|
||||
},
|
||||
{
|
||||
avatar: avatar5,
|
||||
name: 'Julian Murphy',
|
||||
email: 'lunebame@umdomgu.net',
|
||||
permission: 'Can Edit',
|
||||
},
|
||||
{
|
||||
avatar: avatar6,
|
||||
name: 'Sophie Gilbert',
|
||||
email: 'ha@sugit.gov',
|
||||
permission: 'Can View',
|
||||
},
|
||||
{
|
||||
avatar: avatar7,
|
||||
name: 'Chris Watkins',
|
||||
email: 'zokap@mak.org',
|
||||
permission: 'Can Comment',
|
||||
},
|
||||
{
|
||||
avatar: avatar8,
|
||||
name: 'Adelaide Nichols',
|
||||
email: 'ujinomu@jigo.com',
|
||||
permission: 'Can Edit',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:model-value="props.isDialogVisible"
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
@update:model-value="dialogVisibleUpdate"
|
||||
>
|
||||
<!-- 👉 Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="share-project-dialog pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Share Project
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Share project with a team members
|
||||
</p>
|
||||
|
||||
<AppAutocomplete
|
||||
label="Add Members"
|
||||
:items="membersList"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
placeholder="Add project members..."
|
||||
>
|
||||
<template #item="{ props: listItemProp, item }">
|
||||
<VListItem v-bind="listItemProp">
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
:image="item.raw.avatar"
|
||||
size="30"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</AppAutocomplete>
|
||||
|
||||
<h5 class="text-h5 mb-4 mt-6">
|
||||
8 Members
|
||||
</h5>
|
||||
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="member in membersList"
|
||||
:key="member.name"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar :image="member.avatar" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
{{ member.name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ member.email }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
color="secondary"
|
||||
:icon="$vuetify.display.xs"
|
||||
>
|
||||
<span class="d-none d-sm-block me-1">{{ member.permission }}</span>
|
||||
<VIcon icon="tabler-chevron-down" />
|
||||
|
||||
<VMenu activator="parent">
|
||||
<VList :selected="[member.permission]">
|
||||
<VListItem
|
||||
v-for="(item, index) in ['Owner', 'Can Edit', 'Can Comment', 'Can View']"
|
||||
:key="index"
|
||||
:value="item"
|
||||
>
|
||||
<VListItemTitle>{{ item }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-3 mt-6">
|
||||
<h6 class="text-h6 font-weight-medium d-flex align-start">
|
||||
<VIcon
|
||||
icon="tabler-users"
|
||||
class="me-2"
|
||||
size="20"
|
||||
/>
|
||||
<div>Public to Vuexy - Pixinvent</div>
|
||||
</h6>
|
||||
|
||||
<VBtn
|
||||
class="text-capitalize"
|
||||
prepend-icon="tabler-link"
|
||||
>
|
||||
Copy Project Link
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.share-project-dialog {
|
||||
.card-list {
|
||||
--v-card-list-gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
123
resources/js/components/dialogs/TwoFactorAuthDialog.vue
Normal file
123
resources/js/components/dialogs/TwoFactorAuthDialog.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
smsCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
authAppCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
icon: 'tabler-settings',
|
||||
title: 'Authenticator Apps',
|
||||
desc: 'Get code from an app like Google Authenticator or Microsoft Authenticator.',
|
||||
value: 'authApp',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-message',
|
||||
title: 'SMS',
|
||||
desc: 'We will send a code via SMS if you need to use your backup login method.',
|
||||
value: 'sms',
|
||||
},
|
||||
]
|
||||
|
||||
const selectedMethod = ref('authApp')
|
||||
const isAuthAppDialogVisible = ref(false)
|
||||
const isSmsDialogVisible = ref(false)
|
||||
|
||||
const openSelectedMethodDialog = () => {
|
||||
if (selectedMethod.value === 'authApp') {
|
||||
isAuthAppDialogVisible.value = true
|
||||
isSmsDialogVisible.value = false
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
if (selectedMethod.value === 'sms') {
|
||||
isAuthAppDialogVisible.value = false
|
||||
isSmsDialogVisible.value = true
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 800"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Select Authentication Method
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
You also need to select a method by which the proxy authenticates to the directory serve.
|
||||
</p>
|
||||
<CustomRadios
|
||||
v-model:selected-radio="selectedMethod"
|
||||
:radio-content="authMethods"
|
||||
:grid-column="{ cols: '12' }"
|
||||
>
|
||||
<template #default="items">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex gap-1 mb-2">
|
||||
<VIcon
|
||||
:icon="items.item.icon"
|
||||
size="20"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
<h6 class="text-h6">
|
||||
{{ items.item.title }}
|
||||
</h6>
|
||||
</div>
|
||||
<p class="text-body-2 mb-0">
|
||||
{{ items.item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</CustomRadios>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-4 justify-center">
|
||||
<VBtn @click="openSelectedMethodDialog">
|
||||
submit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="$emit('update:isDialogVisible', false)"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<AddAuthenticatorAppDialog
|
||||
v-model:is-dialog-visible="isAuthAppDialogVisible"
|
||||
:auth-code="props.authAppCode"
|
||||
/>
|
||||
<EnableOneTimePasswordDialog
|
||||
v-model:is-dialog-visible="isSmsDialogVisible"
|
||||
:mobile-number="props.smsCode"
|
||||
/>
|
||||
</template>
|
||||
224
resources/js/components/dialogs/UserInfoEditDialog.vue
Normal file
224
resources/js/components/dialogs/UserInfoEditDialog.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
userData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
id: 0,
|
||||
fullName: '',
|
||||
company: '',
|
||||
role: '',
|
||||
username: '',
|
||||
country: '',
|
||||
contact: '',
|
||||
email: '',
|
||||
currentPlan: '',
|
||||
status: '',
|
||||
avatar: '',
|
||||
taskDone: null,
|
||||
projectDone: null,
|
||||
taxId: '',
|
||||
language: '',
|
||||
}),
|
||||
},
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'submit',
|
||||
'update:isDialogVisible',
|
||||
])
|
||||
|
||||
const userData = ref(structuredClone(toRaw(props.userData)))
|
||||
const isUseAsBillingAddress = ref(false)
|
||||
|
||||
watch(() => props, () => {
|
||||
userData.value = structuredClone(toRaw(props.userData))
|
||||
})
|
||||
|
||||
const onFormSubmit = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
emit('submit', userData.value)
|
||||
}
|
||||
|
||||
const onFormReset = () => {
|
||||
userData.value = structuredClone(toRaw(props.userData))
|
||||
emit('update:isDialogVisible', false)
|
||||
}
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 900"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
|
||||
|
||||
<VCard class="pa-sm-10 pa-2">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Edit User Information
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Updating user details will receive a privacy audit.
|
||||
</p>
|
||||
|
||||
<!-- 👉 Form -->
|
||||
<VForm
|
||||
class="mt-6"
|
||||
@submit.prevent="onFormSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<!-- 👉 First Name -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="userData.fullName.split(' ')[0]"
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Last Name -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="userData.fullName.split(' ')[1]"
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Username -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="userData.username"
|
||||
label="Username"
|
||||
placeholder="john.doe.007"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Billing Email -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="userData.email"
|
||||
label="Email"
|
||||
placeholder="johndoe@email.com"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Status -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="userData.status"
|
||||
label="Status"
|
||||
placeholder="Active"
|
||||
:items="['Active', 'Inactive', 'Pending']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Tax Id -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="userData.taxId"
|
||||
label="Tax ID"
|
||||
placeholder="123456789"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Contact -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="userData.contact"
|
||||
label="Phone Number"
|
||||
placeholder="+1 9876543210"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Language -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="userData.language"
|
||||
closable-chips
|
||||
chips
|
||||
multiple
|
||||
label="Language"
|
||||
placeholder="English"
|
||||
:items="['English', 'Spanish', 'French']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Country -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="userData.country"
|
||||
label="Country"
|
||||
placeholder="United States"
|
||||
:items="['United States', 'United Kingdom', 'France']"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Switch -->
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="isUseAsBillingAddress"
|
||||
density="compact"
|
||||
label="Use as a billing address?"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Submit and Cancel -->
|
||||
<VCol
|
||||
cols="12"
|
||||
class="d-flex flex-wrap justify-center gap-4"
|
||||
>
|
||||
<VBtn type="submit">
|
||||
Submit
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
@click="onFormReset"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
114
resources/js/components/dialogs/UserUpgradePlanDialog.vue
Normal file
114
resources/js/components/dialogs/UserUpgradePlanDialog.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:isDialogVisible'])
|
||||
|
||||
const selectedPlan = ref('standard')
|
||||
|
||||
const plansList = [
|
||||
{
|
||||
desc: 'Standard - $99/month',
|
||||
title: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
{
|
||||
desc: 'Basic - $0/month',
|
||||
title: 'Basic',
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
desc: 'Enterprise - $499/month',
|
||||
title: 'Enterprise',
|
||||
value: 'enterprice',
|
||||
},
|
||||
{
|
||||
desc: 'Company - $999/month',
|
||||
title: 'Company',
|
||||
value: 'company',
|
||||
},
|
||||
]
|
||||
|
||||
const isConfirmDialogVisible = ref(false)
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 upgrade plan -->
|
||||
<VDialog
|
||||
:width="$vuetify.display.smAndDown ? 'auto' : 650"
|
||||
:model-value="props.isDialogVisible"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- Dialog close btn -->
|
||||
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
|
||||
|
||||
<VCard class="pa-2 pa-sm-10">
|
||||
<VCardText>
|
||||
<!-- 👉 Title -->
|
||||
<h4 class="text-h4 text-center mb-2">
|
||||
Upgrade Plan
|
||||
</h4>
|
||||
<p class="text-body-1 text-center mb-6">
|
||||
Choose the best plan for user.
|
||||
</p>
|
||||
|
||||
<div class="d-flex justify-space-between flex-column flex-sm-row gap-4">
|
||||
<AppSelect
|
||||
v-model="selectedPlan"
|
||||
:items="plansList"
|
||||
label="Choose a plan"
|
||||
placeholder="Basic"
|
||||
/>
|
||||
<VBtn
|
||||
class="align-self-end"
|
||||
:block="$vuetify.display.xs"
|
||||
>
|
||||
Upgrade
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VDivider class="my-6" />
|
||||
|
||||
<p class="text-body-1 mb-1">
|
||||
User current plan is standard plan
|
||||
</p>
|
||||
<div class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center gap-1 me-3">
|
||||
<sup class="text-body-1 text-primary">$</sup>
|
||||
<h1 class="text-h1 text-primary">
|
||||
99
|
||||
</h1>
|
||||
<sub class="text-body-2 mt-5">
|
||||
/ month
|
||||
</sub>
|
||||
</div>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="isConfirmDialogVisible = true"
|
||||
>
|
||||
Cancel Subscription
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Confirm Dialog -->
|
||||
<ConfirmDialog
|
||||
v-model:is-dialog-visible="isConfirmDialogVisible"
|
||||
cancel-title="Cancelled"
|
||||
confirm-title="Unsubscribed!"
|
||||
confirm-msg="Your subscription cancelled successfully."
|
||||
confirmation-question="Are you sure to cancel your subscription?"
|
||||
cancel-msg="Unsubscription Cancelled!!"
|
||||
/>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user