mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 04:01:10 -04:00
v6 update
This commit is contained in:
12
resources/scripts/customer/views/BaseCheckon.vue
Normal file
12
resources/scripts/customer/views/BaseCheckon.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="h-10 w-20 bg-gray-100">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
15
resources/scripts/customer/views/SamplePage.vue
Normal file
15
resources/scripts/customer/views/SamplePage.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="bg-blue-100 h-screen container mx-auto px-6">
|
||||
<h1 class="text-xl font-bold">Samplez Pages</h1>
|
||||
<BaseButton>Hello </BaseButton>
|
||||
<BaseCheckon>{{ customerStore.customers }} </BaseCheckon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import BaseCheckon from '@/scripts/customer/BaseCheckon.vue'
|
||||
import { useCustomerStore } from '@/scripts/customer/stores/customer'
|
||||
|
||||
const customerStore = useCustomerStore()
|
||||
</script>
|
||||
100
resources/scripts/customer/views/auth/ForgotPassword.vue
Normal file
100
resources/scripts/customer/views/auth/ForgotPassword.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<form id="loginForm" @submit.prevent="validateBeforeSubmit">
|
||||
<BaseInputGroup
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
:label="$t('login.enter_email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
name="email"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
<div v-if="!isSent">
|
||||
{{ $t('validation.send_reset_link') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('validation.not_yet') }}
|
||||
</div>
|
||||
</BaseButton>
|
||||
|
||||
<div class="mt-4 mb-4 text-sm">
|
||||
<router-link
|
||||
to="login"
|
||||
class="text-sm text-primary-400 hover:text-gray-700"
|
||||
>
|
||||
{{ $t('general.back_to_login') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/scripts/customer/stores/auth'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
// // store
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
// local state
|
||||
|
||||
const formData = reactive({
|
||||
email: '',
|
||||
company: '',
|
||||
})
|
||||
const isSent = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// validation
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, formData)
|
||||
|
||||
// methods
|
||||
|
||||
function validateBeforeSubmit(e) {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
isLoading.value = true
|
||||
let data = {
|
||||
...formData,
|
||||
company: route.params.company,
|
||||
}
|
||||
|
||||
authStore
|
||||
.forgotPassword(data)
|
||||
.then((res) => {
|
||||
isLoading.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isLoading.value = false
|
||||
})
|
||||
isSent.value = true
|
||||
}
|
||||
</script>
|
||||
139
resources/scripts/customer/views/auth/Login.vue
Normal file
139
resources/scripts/customer/views/auth/Login.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<form
|
||||
id="loginForm"
|
||||
class="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
@submit.prevent="validateBeforeSubmit"
|
||||
>
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.loginData.email.$error && v$.loginData.email.$errors[0].$message
|
||||
"
|
||||
:label="$t('login.email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="authStore.loginData.email"
|
||||
type="email"
|
||||
:invalid="v$.loginData.email.$error"
|
||||
@input="v$.loginData.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.loginData.password.$error &&
|
||||
v$.loginData.password.$errors[0].$message
|
||||
"
|
||||
:label="$t('login.password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="authStore.loginData.password"
|
||||
:type="getInputType"
|
||||
:invalid="v$.loginData.password.$error"
|
||||
@input="v$.loginData.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
name="EyeOffIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
</template>
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
<div class="flex items-center justify-between">
|
||||
<router-link
|
||||
:to="{ name: 'customer.forgot-password' }"
|
||||
class="text-sm text-primary-600 hover:text-gray-500"
|
||||
>
|
||||
{{ $t('login.forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
type="submit"
|
||||
class="w-full justify-center"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="LockClosedIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('login.login') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useAuthStore } from '@/scripts/customer/stores/auth'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
const isShowPassword = ref(false)
|
||||
|
||||
const getInputType = computed(() => {
|
||||
if (isShowPassword.value) {
|
||||
return 'text'
|
||||
}
|
||||
return 'password'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
loginData: {
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, authStore)
|
||||
|
||||
async function validateBeforeSubmit() {
|
||||
v$.value.loginData.$touch()
|
||||
if (v$.value.loginData.$invalid) {
|
||||
return true
|
||||
}
|
||||
isLoading.value = true
|
||||
let data = {
|
||||
...authStore.loginData,
|
||||
company: route.params.company,
|
||||
}
|
||||
|
||||
try {
|
||||
await authStore.login(data)
|
||||
isLoading.value = false
|
||||
return router.push({ name: 'customer.dashboard' })
|
||||
authStore.$reset()
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
141
resources/scripts/customer/views/auth/ResetPassword.vue
Normal file
141
resources/scripts/customer/views/auth/ResetPassword.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<form id="loginForm" @submit.prevent="onSubmit">
|
||||
<BaseInputGroup
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
:label="$t('login.email')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="loginData.email"
|
||||
type="email"
|
||||
name="email"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
:label="$t('login.password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="loginData.password"
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
:invalid="v$.password.$error"
|
||||
@input="v$.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<EyeOffIcon
|
||||
v-if="isShowPassword"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<EyeIcon
|
||||
v-else
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/> </template
|
||||
></BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.password_confirmation.$error &&
|
||||
v$.password_confirmation.$errors[0].$message
|
||||
"
|
||||
:label="$t('login.retype_password')"
|
||||
class="mb-4"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="loginData.password_confirmation"
|
||||
type="password"
|
||||
name="password"
|
||||
:invalid="v$.password_confirmation.$error"
|
||||
@input="v$.password_confirmation.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton type="submit" variant="primary">
|
||||
{{ $t('login.reset_password') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import {
|
||||
required,
|
||||
helpers,
|
||||
minLength,
|
||||
sameAs,
|
||||
email,
|
||||
} from '@vuelidate/validators'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/scripts/customer/stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const loginData = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
let isShowPassword = ref(false)
|
||||
let isLoading = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.password_min_length', { count: 8 }),
|
||||
minLength(8)
|
||||
),
|
||||
},
|
||||
password_confirmation: {
|
||||
sameAsPassword: helpers.withMessage(
|
||||
t('validation.password_incorrect'),
|
||||
sameAs(loginData.password)
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, loginData)
|
||||
|
||||
async function onSubmit(e) {
|
||||
v$.value.$touch()
|
||||
|
||||
if (!v$.value.$invalid) {
|
||||
let data = {
|
||||
email: loginData.email,
|
||||
password: loginData.password,
|
||||
password_confirmation: loginData.password_confirmation,
|
||||
token: route.params.token,
|
||||
}
|
||||
isLoading.value = true
|
||||
let res = authStore.resetPassword(data, route.params.company)
|
||||
isLoading.value = false
|
||||
if (res.data) {
|
||||
router.push({ name: 'customer.login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
11
resources/scripts/customer/views/dashboard/Dashboard.vue
Normal file
11
resources/scripts/customer/views/dashboard/Dashboard.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<DashboardStats />
|
||||
<DashboardTable />
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DashboardStats from '@/scripts/customer/views/dashboard/DashboardStats.vue'
|
||||
import DashboardTable from '@/scripts/customer/views/dashboard/DashboardTable.vue'
|
||||
</script>
|
||||
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-9 xl:gap-8">
|
||||
<!-- Amount Due -->
|
||||
<DashboardStatsItem
|
||||
:icon-component="DollarIcon"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
:route="{ name: 'invoices.dashboard' }"
|
||||
:large="true"
|
||||
:label="$t('dashboard.cards.due_amount')"
|
||||
>
|
||||
<BaseFormatMoney
|
||||
:amount="dashboardStore.totalDueAmount"
|
||||
:currency="globalStore.currency"
|
||||
/>
|
||||
</DashboardStatsItem>
|
||||
|
||||
<!-- Invoices -->
|
||||
<DashboardStatsItem
|
||||
:icon-component="InvoiceIcon"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
:route="{ name: 'invoices.dashboard' }"
|
||||
:label="$t('dashboard.cards.invoices')"
|
||||
>
|
||||
{{ dashboardStore.invoiceCount }}
|
||||
</DashboardStatsItem>
|
||||
|
||||
<!-- Estimates -->
|
||||
<DashboardStatsItem
|
||||
:icon-component="EstimateIcon"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
:route="{ name: 'estimates.dashboard' }"
|
||||
:label="$t('dashboard.cards.estimates')"
|
||||
>
|
||||
{{ dashboardStore.estimateCount }}
|
||||
</DashboardStatsItem>
|
||||
|
||||
<!-- Payments -->
|
||||
|
||||
<DashboardStatsItem
|
||||
:icon-component="PaymentIcon"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
:route="{ name: 'payments.dashboard' }"
|
||||
:label="$t('dashboard.cards.payments')"
|
||||
>
|
||||
{{ dashboardStore.paymentCount }}
|
||||
</DashboardStatsItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue'
|
||||
import DollarIcon from '@/scripts/components/icons/dashboard/DollarIcon.vue'
|
||||
import InvoiceIcon from '@/scripts/components/icons/dashboard/InvoiceIcon.vue'
|
||||
import PaymentIcon from '@/scripts/components/icons/dashboard/PaymentIcon.vue'
|
||||
import EstimateIcon from '@/scripts/components/icons/dashboard/EstimateIcon.vue'
|
||||
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useDashboardStore } from '@/scripts/customer/stores/dashboard'
|
||||
import DashboardStatsItem from '@/scripts/customer/views/dashboard/DashboardStatsItem.vue'
|
||||
//store
|
||||
|
||||
const utils = inject('utils')
|
||||
const globalStore = useGlobalStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
dashboardStore.loadData()
|
||||
</script>
|
||||
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<router-link
|
||||
v-if="!loading"
|
||||
class="
|
||||
relative
|
||||
flex
|
||||
justify-between
|
||||
p-3
|
||||
bg-white
|
||||
rounded
|
||||
shadow
|
||||
hover:bg-gray-50
|
||||
xl:p-4
|
||||
lg:col-span-2
|
||||
"
|
||||
:class="{ 'lg:!col-span-3': large }"
|
||||
:to="route"
|
||||
>
|
||||
<div>
|
||||
<span class="text-xl font-semibold leading-tight text-black xl:text-3xl">
|
||||
<slot />
|
||||
</span>
|
||||
<span class="block mt-1 text-sm leading-tight text-gray-500 xl:text-lg">
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<component :is="iconComponent" class="w-10 h-10 xl:w-12 xl:h-12" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<StatsCardPlaceholder v-else-if="large" />
|
||||
|
||||
<StatsCardSmPlaceholder v-else />
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import StatsCardPlaceholder from '@/scripts/customer/views/dashboard/DashboardStatsPlaceholder.vue'
|
||||
import StatsCardSmPlaceholder from '@/scripts/customer/views/dashboard/DashboardStatsSmPlaceholder.vue'
|
||||
|
||||
defineProps({
|
||||
iconComponent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
route: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<BaseContentPlaceholders
|
||||
:rounded="true"
|
||||
class="relative flex justify-between w-full p-3 bg-white rounded shadow lg:col-span-3 xl:p-4"
|
||||
>
|
||||
<div>
|
||||
<BaseContentPlaceholdersText
|
||||
class="h-5 -mb-1 w-14 xl:mb-6 xl:h-7"
|
||||
:lines="1"
|
||||
/>
|
||||
<BaseContentPlaceholdersText class="h-3 w-28 xl:h-4" :lines="1" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<BaseContentPlaceholdersBox
|
||||
:circle="true"
|
||||
class="w-10 h-10 xl:w-12 xl:h-12"
|
||||
/>
|
||||
</div>
|
||||
</BaseContentPlaceholders>
|
||||
</template>
|
||||
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<BaseContentPlaceholders
|
||||
:rounded="true"
|
||||
class="
|
||||
relative
|
||||
flex
|
||||
justify-between
|
||||
w-full
|
||||
p-3
|
||||
bg-white
|
||||
rounded
|
||||
shadow
|
||||
lg:col-span-2
|
||||
xl:p-4
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<BaseContentPlaceholdersText
|
||||
class="w-12 h-5 -mb-1 xl:mb-6 xl:h-7"
|
||||
:lines="1"
|
||||
/>
|
||||
<BaseContentPlaceholdersText class="w-20 h-3 xl:h-4" :lines="1" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<BaseContentPlaceholdersBox
|
||||
:circle="true"
|
||||
class="w-10 h-10 xl:w-12 xl:h-12"
|
||||
/>
|
||||
</div>
|
||||
</BaseContentPlaceholders>
|
||||
</template>
|
||||
149
resources/scripts/customer/views/dashboard/DashboardTable.vue
Normal file
149
resources/scripts/customer/views/dashboard/DashboardTable.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-6 mt-10 xl:grid-cols-2">
|
||||
<!-- Due Invoices -->
|
||||
<div class="due-invoices">
|
||||
<div class="relative z-10 flex items-center justify-between mb-3">
|
||||
<h6 class="mb-0 text-xl font-semibold leading-normal">
|
||||
{{ $t('dashboard.recent_invoices_card.title') }}
|
||||
</h6>
|
||||
|
||||
<BaseButton
|
||||
size="sm"
|
||||
variant="primary-outline"
|
||||
@click="$router.push({ name: 'invoices.dashboard' })"
|
||||
>
|
||||
{{ $t('dashboard.recent_invoices_card.view_all') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<!-- Recent Invoice-->
|
||||
<BaseTable
|
||||
:data="dashboardStore.recentInvoices"
|
||||
:columns="dueInvoiceColumns"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
>
|
||||
<template #cell-invoice_number="{ row }">
|
||||
<router-link
|
||||
:to="{
|
||||
path: `/${globalStore.companySlug}/customer/invoices/${row.data.id}/view`,
|
||||
}"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.data.invoice_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template #cell-paid_status="{ row }">
|
||||
<BasePaidStatusBadge :status="row.data.paid_status">
|
||||
{{ row.data.paid_status }}
|
||||
</BasePaidStatusBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-due_amount="{ row }">
|
||||
<BaseFormatMoney
|
||||
:amount="row.data.due_amount"
|
||||
:currency="globalStore.currency"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
<!-- Recent Estimates -->
|
||||
<div class="recent-estimates">
|
||||
<div class="relative z-10 flex items-center justify-between mb-3">
|
||||
<h6 class="mb-0 text-xl font-semibold leading-normal">
|
||||
{{ $t('dashboard.recent_estimate_card.title') }}
|
||||
</h6>
|
||||
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
size="sm"
|
||||
@click="$router.push({ name: 'estimates.dashboard' })"
|
||||
>
|
||||
{{ $t('dashboard.recent_estimate_card.view_all') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<BaseTable
|
||||
:data="dashboardStore.recentEstimates"
|
||||
:columns="recentEstimateColumns"
|
||||
:loading="!globalStore.getDashboardDataLoaded"
|
||||
>
|
||||
<template #cell-estimate_number="{ row }">
|
||||
<router-link
|
||||
:to="{
|
||||
path: `/${globalStore.companySlug}/customer/estimates/${row.data.id}/view`,
|
||||
}"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.data.estimate_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
<template #cell-status="{ row }">
|
||||
<BaseEstimateStatusBadge :status="row.data.status" class="px-3 py-1">
|
||||
{{ row.data.status }}
|
||||
</BaseEstimateStatusBadge>
|
||||
</template>
|
||||
<template #cell-total="{ row }">
|
||||
<BaseFormatMoney
|
||||
:amount="row.data.total"
|
||||
:currency="globalStore.currency"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useDashboardStore } from '@/scripts/customer/stores/dashboard'
|
||||
import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue'
|
||||
|
||||
// store
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
const { tm, t } = useI18n()
|
||||
const utils = inject('utils')
|
||||
const route = useRoute()
|
||||
|
||||
//computed prop
|
||||
|
||||
const dueInvoiceColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'formattedDueDate',
|
||||
label: t('dashboard.recent_invoices_card.due_on'),
|
||||
},
|
||||
{
|
||||
key: 'invoice_number',
|
||||
label: t('invoices.number'),
|
||||
},
|
||||
{ key: 'paid_status', label: t('invoices.status') },
|
||||
{
|
||||
key: 'due_amount',
|
||||
label: t('dashboard.recent_invoices_card.amount_due'),
|
||||
},
|
||||
]
|
||||
})
|
||||
const recentEstimateColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'formattedEstimateDate',
|
||||
label: t('dashboard.recent_estimate_card.date'),
|
||||
},
|
||||
{
|
||||
key: 'estimate_number',
|
||||
label: t('estimates.number'),
|
||||
},
|
||||
{ key: 'status', label: t('estimates.status') },
|
||||
{
|
||||
key: 'total',
|
||||
label: t('dashboard.recent_estimate_card.amount_due'),
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
270
resources/scripts/customer/views/estimates/Index.vue
Normal file
270
resources/scripts/customer/views/estimates/Index.vue
Normal file
@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<!-- Page Header -->
|
||||
<BasePageHeader :title="$t('estimates.title')">
|
||||
<BaseBreadcrumb>
|
||||
<BaseBreadcrumbItem
|
||||
:title="$t('general.home')"
|
||||
:to="`/${globalStore.companySlug}/customer/dashboard`"
|
||||
/>
|
||||
<BaseBreadcrumbItem
|
||||
:title="$tc('estimates.estimate', 2)"
|
||||
to="#"
|
||||
active
|
||||
/>
|
||||
</BaseBreadcrumb>
|
||||
<template #actions>
|
||||
<BaseButton
|
||||
v-if="estimateStore.totalEstimates"
|
||||
variant="primary-outline"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
<template #right="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!showFilters"
|
||||
name="FilterIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
<BaseIcon v-else name="XIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<BaseFilterWrapper v-show="showFilters" @clear="clearFilter">
|
||||
<BaseInputGroup :label="$t('estimates.status')" class="px-3">
|
||||
<BaseSelectInput
|
||||
v-model="filters.status"
|
||||
:options="status"
|
||||
searchable
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_a_status')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('estimates.estimate_number')"
|
||||
color="black-light"
|
||||
class="px-3 mt-2"
|
||||
>
|
||||
<BaseInput v-model="filters.estimate_number">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
<BaseIcon name="HashtagIcon" class="h-5 mr-3 text-gray-600" />
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('general.from')" class="px-3">
|
||||
<BaseDatePicker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div
|
||||
class="hidden w-8 h-0 mx-4 border border-gray-400 border-solid xl:block"
|
||||
style="margin-top: 1.5rem"
|
||||
/>
|
||||
|
||||
<BaseInputGroup :label="$t('general.to')" class="px-3">
|
||||
<BaseDatePicker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseFilterWrapper>
|
||||
|
||||
<BaseEmptyPlaceholder
|
||||
v-if="showEmptyScreen"
|
||||
:title="$t('estimates.no_estimates')"
|
||||
:description="$t('estimates.list_of_estimates')"
|
||||
>
|
||||
<ObservatoryIcon class="mt-5 mb-4" />
|
||||
</BaseEmptyPlaceholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="estimateColumns"
|
||||
:placeholder-count="estimateStore.totalEstimates >= 20 ? 10 : 5"
|
||||
class="mt-10"
|
||||
>
|
||||
<template #cell-estimate_date="{ row }">
|
||||
{{ row.data.formatted_estimate_date }}
|
||||
</template>
|
||||
|
||||
<template #cell-estimate_number="{ row }">
|
||||
<router-link
|
||||
:to="{ path: `estimates/${row.data.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.data.estimate_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<BaseEstimateStatusBadge :status="row.data.status" class="px-3 py-1">
|
||||
{{ row.data.status }}
|
||||
</BaseEstimateStatusBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-total="{ row }">
|
||||
<BaseFormatMoney :amount="row.data.total" />
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
<router-link :to="`estimates/${row.data.id}/view`">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon name="EyeIcon" class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { debouncedWatch } from '@vueuse/core'
|
||||
import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue'
|
||||
import { ref, computed, reactive, inject } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useEstimateStore } from '@/scripts/customer/stores/estimate'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ObservatoryIcon from '@/scripts/components/icons/empty/ObservatoryIcon.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// utils
|
||||
|
||||
const utils = inject('utils')
|
||||
const route = useRoute()
|
||||
// Local state
|
||||
const table = ref(null)
|
||||
let showFilters = ref(false)
|
||||
let isFetchingInitialData = ref(true)
|
||||
|
||||
const status = ref([
|
||||
'DRAFT',
|
||||
'SENT',
|
||||
'VIEWED',
|
||||
'EXPIRED',
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
])
|
||||
const filters = reactive({
|
||||
status: '',
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
estimate_number: '',
|
||||
})
|
||||
|
||||
// store
|
||||
const globalStore = useGlobalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
// computed
|
||||
|
||||
const estimateColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'estimate_date',
|
||||
label: t('estimates.date'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{ key: 'estimate_number', label: t('estimates.number', 2) },
|
||||
{ key: 'status', label: t('estimates.status') },
|
||||
{ key: 'total', label: t('estimates.total') },
|
||||
{
|
||||
key: 'actions',
|
||||
thClass: 'text-right',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const showEmptyScreen = computed(() => {
|
||||
return !estimateStore.totalEstimates && !isFetchingInitialData.value
|
||||
})
|
||||
|
||||
const currency = computed(() => {
|
||||
return globalStore.currency
|
||||
})
|
||||
// watch
|
||||
|
||||
debouncedWatch(
|
||||
filters,
|
||||
() => {
|
||||
setFilters()
|
||||
},
|
||||
{ debounce: 500 }
|
||||
)
|
||||
|
||||
// methods
|
||||
|
||||
function refreshTable() {
|
||||
table.value.refresh()
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
refreshTable()
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
filters.status = ''
|
||||
filters.from_date = ''
|
||||
filters.to_date = ''
|
||||
filters.estimate_number = ''
|
||||
}
|
||||
|
||||
function toggleFilter() {
|
||||
if (showFilters.value) {
|
||||
clearFilter()
|
||||
}
|
||||
|
||||
showFilters.value = !showFilters.value
|
||||
}
|
||||
|
||||
async function fetchData({ page, sort }) {
|
||||
let data = {
|
||||
status: filters.status,
|
||||
estimate_number: filters.estimate_number,
|
||||
from_date: filters.from_date,
|
||||
to_date: filters.to_date,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
isFetchingInitialData.value = true
|
||||
|
||||
let response = await estimateStore.fetchEstimate(
|
||||
data,
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
437
resources/scripts/customer/views/estimates/View.vue
Normal file
437
resources/scripts/customer/views/estimates/View.vue
Normal file
@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<BasePage class="xl:pl-96">
|
||||
<BasePageHeader :title="pageTitle.estimate_number">
|
||||
<template #actions>
|
||||
<div class="mr-3 text-sm">
|
||||
<BaseButton
|
||||
v-if="estimateStore.selectedViewEstimate.status === 'DRAFT'"
|
||||
variant="primary"
|
||||
@click="AcceptEstimate"
|
||||
>
|
||||
{{ $t('estimates.accept_estimate') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="mr-3 text-sm">
|
||||
<BaseButton
|
||||
v-if="estimateStore.selectedViewEstimate.status === 'DRAFT'"
|
||||
variant="primary-outline"
|
||||
@click="RejectEstimate"
|
||||
>
|
||||
{{ $t('estimates.reject_estimate') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 bg-white w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
px-4
|
||||
pt-8
|
||||
pb-6
|
||||
border border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="searchData.estimate_number"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon name="SearchIcon" class="h-5 text-gray-400" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
|
||||
<div class="flex ml-3" role="group" aria-label="First group">
|
||||
<BaseDropdown
|
||||
position="bottom-start"
|
||||
width-class="w-50"
|
||||
position-class="left-0"
|
||||
>
|
||||
<template #activator>
|
||||
<BaseButton variant="gray">
|
||||
<BaseIcon name="FilterIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="
|
||||
px-4
|
||||
py-1
|
||||
pb-2
|
||||
mb-2
|
||||
text-sm
|
||||
border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_estimate_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('reports.estimates.estimate_date')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="estimate_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_due_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('estimates.due_date')"
|
||||
value="expiry_date"
|
||||
size="sm"
|
||||
name="filter"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_estimate_number"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('estimates.estimate_number')"
|
||||
value="estimate_number"
|
||||
size="sm"
|
||||
name="filter"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
</BaseDropdown>
|
||||
|
||||
<BaseButton class="ml-1" variant="white" @click="sortData">
|
||||
<BaseIcon v-if="getOrderBy" name="SortAscendingIcon" class="h-5" />
|
||||
<BaseIcon v-else name="SortDescendingIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
h-full
|
||||
pb-32
|
||||
overflow-y-scroll
|
||||
border-l border-gray-200 border-solid
|
||||
sw-scroll
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
v-for="(estimate, index) in estimateStore.estimates"
|
||||
:id="'estimate-' + estimate.id"
|
||||
:key="index"
|
||||
:to="`/${globalStore.companySlug}/customer/estimates/${estimate.id}/view`"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid':
|
||||
hasActiveUrl(estimate.id),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
class="
|
||||
mb-1
|
||||
text-md
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-gray-500
|
||||
capitalize
|
||||
"
|
||||
>
|
||||
{{ estimate.estimate_number }}
|
||||
</div>
|
||||
|
||||
<BaseEstimateStatusBadge :status="estimate.status">
|
||||
{{ estimate.status }}
|
||||
</BaseEstimateStatusBadge>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 whitespace-nowrap right">
|
||||
<BaseFormatMoney
|
||||
class="
|
||||
mb-2
|
||||
text-xl
|
||||
not-italic
|
||||
font-semibold
|
||||
leading-8
|
||||
text-right text-gray-900
|
||||
block
|
||||
"
|
||||
:amount="estimate.total"
|
||||
:currency="estimate.currency"
|
||||
/>
|
||||
<div class="text-sm text-right text-gray-500 non-italic">
|
||||
{{ estimate.formatted_estimate_date }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<p
|
||||
v-if="!estimateStore.estimates.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('estimates.no_matching_estimates') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- pdf -->
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
v-if="shareableLink"
|
||||
:src="shareableLink"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue'
|
||||
import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { ref, reactive, computed, inject, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import moment from 'moment'
|
||||
import { useEstimateStore } from '@/scripts/customer/stores/estimate'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
|
||||
// Router
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// store
|
||||
const estimateStore = useEstimateStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { tm, t } = useI18n()
|
||||
|
||||
// local state
|
||||
let estimate = reactive({})
|
||||
let searchData = reactive({
|
||||
orderBy: '',
|
||||
orderByField: '',
|
||||
estimate_number: '',
|
||||
})
|
||||
|
||||
let isSearching = ref(false)
|
||||
|
||||
//Utils
|
||||
|
||||
const utils = inject('utils')
|
||||
//Store
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// Computed Props
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
return estimateStore.selectedViewEstimate
|
||||
})
|
||||
|
||||
const getOrderBy = computed(() => {
|
||||
if (searchData.orderBy === 'asc' || searchData.orderBy == null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const getOrderName = computed(() =>
|
||||
getOrderBy.value ? tm('general.ascending') : tm('general.descending')
|
||||
)
|
||||
|
||||
const shareableLink = computed(() => {
|
||||
return estimate.unique_hash ? `/estimates/pdf/${estimate.unique_hash}` : false
|
||||
})
|
||||
|
||||
// Watcher
|
||||
|
||||
watch(route, () => {
|
||||
loadEstimate()
|
||||
})
|
||||
|
||||
// Created
|
||||
|
||||
loadEstimates()
|
||||
loadEstimate()
|
||||
|
||||
onSearch = debounce(onSearch, 500)
|
||||
|
||||
// Methods
|
||||
|
||||
function hasActiveUrl(id) {
|
||||
return route.params.id == id
|
||||
}
|
||||
|
||||
async function loadEstimates() {
|
||||
await estimateStore.fetchEstimate({ limit: 'all' }, globalStore.companySlug)
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToEstimate()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function loadEstimate() {
|
||||
if (route && route.params.id) {
|
||||
let response = await estimateStore.fetchViewEstimate(
|
||||
{
|
||||
id: route.params.id,
|
||||
},
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
if (response.data) {
|
||||
Object.assign(estimate, response.data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToEstimate() {
|
||||
const el = document.getElementById(`estimate-${route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
}
|
||||
|
||||
async function onSearch() {
|
||||
let data = {}
|
||||
|
||||
if (
|
||||
searchData.estimate_number !== '' &&
|
||||
searchData.estimate_number !== null &&
|
||||
searchData.estimate_number !== undefined
|
||||
) {
|
||||
data.estimate_number = searchData.estimate_number
|
||||
}
|
||||
|
||||
if (searchData.orderBy !== null && searchData.orderBy !== undefined) {
|
||||
data.orderBy = searchData.orderBy
|
||||
}
|
||||
|
||||
if (
|
||||
searchData.orderByField !== null &&
|
||||
searchData.orderByField !== undefined
|
||||
) {
|
||||
data.orderByField = searchData.orderByField
|
||||
}
|
||||
|
||||
isSearching.value = true
|
||||
try {
|
||||
let response = await estimateStore.searchEstimate(
|
||||
data,
|
||||
globalStore.companySlug
|
||||
)
|
||||
isSearching.value = false
|
||||
|
||||
if (response.data.data) {
|
||||
estimateStore.estimates = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function sortData() {
|
||||
if (searchData.orderBy === 'asc') {
|
||||
searchData.orderBy = 'desc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
searchData.orderBy = 'asc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
async function AcceptEstimate() {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_accepted', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then(async (res) => {
|
||||
let data = {
|
||||
slug: globalStore.companySlug,
|
||||
id: route.params.id,
|
||||
status: 'ACCEPTED',
|
||||
}
|
||||
if (res) {
|
||||
estimateStore.acceptEstimate(data)
|
||||
router.push({ name: 'estimates.dashboard' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function RejectEstimate() {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_rejected', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then(async (res) => {
|
||||
let data = {
|
||||
slug: globalStore.companySlug,
|
||||
id: route.params.id,
|
||||
status: 'REJECTED',
|
||||
}
|
||||
if (res) {
|
||||
estimateStore.rejectEstimate(data)
|
||||
router.push({ name: 'estimates.dashboard' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${estimate?.unique_hash}`
|
||||
utils.copyTextToClipboard(pdfUrl)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: tm('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
275
resources/scripts/customer/views/invoices/Index.vue
Normal file
275
resources/scripts/customer/views/invoices/Index.vue
Normal file
@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<!-- Page Header -->
|
||||
<BasePageHeader :title="$t('invoices.title')">
|
||||
<BaseBreadcrumb>
|
||||
<BaseBreadcrumbItem
|
||||
:title="$t('general.home')"
|
||||
:to="`/${globalStore.companySlug}/customer/dashboard`"
|
||||
/>
|
||||
<BaseBreadcrumbItem :title="$tc('invoices.invoice', 2)" to="#" active />
|
||||
</BaseBreadcrumb>
|
||||
<template #actions>
|
||||
<BaseButton
|
||||
v-show="invoiceStore.totalInvoices"
|
||||
variant="primary-outline"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
<template #right="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!showFilters"
|
||||
name="FilterIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
<BaseIcon v-else name="XIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<BaseFilterWrapper v-show="showFilters" @clear="clearFilter">
|
||||
<BaseInputGroup :label="$t('invoices.status')" class="px-3">
|
||||
<BaseSelectInput
|
||||
v-model="filters.status"
|
||||
:options="status"
|
||||
searchable
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_a_status')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('invoices.invoice_number')"
|
||||
color="black-light"
|
||||
class="px-3 mt-2"
|
||||
>
|
||||
<BaseInput v-model="filters.invoice_number">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
<BaseIcon name="HashtagIcon" class="h-5 ml-3 text-gray-600" />
|
||||
</BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('general.from')" class="px-3">
|
||||
<BaseDatePicker
|
||||
v-model="filters.from_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div
|
||||
class="hidden w-8 h-0 mx-4 border border-gray-400 border-solid xl:block"
|
||||
style="margin-top: 1.5rem"
|
||||
/>
|
||||
|
||||
<BaseInputGroup :label="$t('general.to')" class="px-3">
|
||||
<BaseDatePicker
|
||||
v-model="filters.to_date"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseFilterWrapper>
|
||||
|
||||
<BaseEmptyPlaceholder
|
||||
v-if="showEmptyScreen"
|
||||
:title="$t('invoices.no_invoices')"
|
||||
:description="$t('invoices.list_of_invoices')"
|
||||
>
|
||||
<MoonwalkerIcon class="mt-5 mb-4" />
|
||||
</BaseEmptyPlaceholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="itemColumns"
|
||||
:placeholder-count="invoiceStore.totalInvoices >= 20 ? 10 : 5"
|
||||
class="mt-10"
|
||||
>
|
||||
<template #cell-invoice_date="{ row }">
|
||||
{{ row.data.formatted_invoice_date }}
|
||||
</template>
|
||||
|
||||
<template #cell-invoice_number="{ row }">
|
||||
<router-link
|
||||
:to="{ path: `invoices/${row.data.id}/view` }"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.data.invoice_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template #cell-due_amount="{ row }">
|
||||
<BaseFormatMoney
|
||||
:amount="row.data.total"
|
||||
:currency="row.data.customer.currency"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<BaseInvoiceStatusBadge :status="row.data.status" class="px-3 py-1">
|
||||
{{ row.data.status }}
|
||||
</BaseInvoiceStatusBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-paid_status="{ row }">
|
||||
<BaseInvoiceStatusBadge
|
||||
:status="row.data.paid_status"
|
||||
class="px-3 py-1"
|
||||
>
|
||||
{{ row.data.paid_status }}
|
||||
</BaseInvoiceStatusBadge>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
<router-link :to="`invoices/${row.data.id}/view`">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon name="EyeIcon" class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/customer/stores/invoice'
|
||||
import { debouncedWatch } from '@vueuse/core'
|
||||
import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue'
|
||||
import { ref, computed, reactive, inject, onMounted } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useRoute } from 'vue-router'
|
||||
import MoonwalkerIcon from '@/scripts/components/icons/empty/MoonwalkerIcon.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
//Utils
|
||||
const utils = inject('utils')
|
||||
const route = useRoute()
|
||||
// local state
|
||||
const table = ref(null)
|
||||
let isFetchingInitialData = ref(true)
|
||||
let showFilters = ref(false)
|
||||
const status = ref(['DRAFT', 'DUE', 'SENT', 'VIEWED', 'OVERDUE', 'COMPLETED'])
|
||||
const filters = reactive({
|
||||
status: '',
|
||||
from_date: '',
|
||||
to_date: '',
|
||||
invoice_number: '',
|
||||
})
|
||||
|
||||
// store
|
||||
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// Invoice Table columns Data
|
||||
|
||||
const currency = computed(() => {
|
||||
return globalStore.currency
|
||||
})
|
||||
|
||||
const itemColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'invoice_date',
|
||||
label: t('invoices.date'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{ key: 'invoice_number', label: t('invoices.number') },
|
||||
|
||||
{ key: 'status', label: t('invoices.status') },
|
||||
{ key: 'paid_status', label: t('invoices.paid_status') },
|
||||
{
|
||||
key: 'due_amount',
|
||||
label: t('dashboard.recent_invoices_card.amount_due'),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
thClass: 'text-right',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// computed props
|
||||
|
||||
const showEmptyScreen = computed(() => {
|
||||
return !invoiceStore.totalInvoices && !isFetchingInitialData.value
|
||||
})
|
||||
|
||||
//watch
|
||||
|
||||
debouncedWatch(
|
||||
filters,
|
||||
() => {
|
||||
setFilters()
|
||||
},
|
||||
{ debounce: 500 }
|
||||
)
|
||||
|
||||
//methods
|
||||
|
||||
function refreshTable() {
|
||||
table.value.refresh()
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
refreshTable()
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
filters.status = ''
|
||||
filters.from_date = ''
|
||||
filters.to_date = ''
|
||||
filters.invoice_number = ''
|
||||
}
|
||||
|
||||
function toggleFilter() {
|
||||
if (showFilters.value) {
|
||||
clearFilter()
|
||||
}
|
||||
|
||||
showFilters.value = !showFilters.value
|
||||
}
|
||||
|
||||
async function fetchData({ page, sort }) {
|
||||
let data = {
|
||||
status: filters.status,
|
||||
invoice_number: filters.invoice_number,
|
||||
from_date: filters.from_date,
|
||||
to_date: filters.to_date,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
isFetchingInitialData.value = true
|
||||
|
||||
let response = await invoiceStore.fetchInvoices(data, globalStore.companySlug)
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
405
resources/scripts/customer/views/invoices/View.vue
Normal file
405
resources/scripts/customer/views/invoices/View.vue
Normal file
@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<BasePage class="xl:pl-96">
|
||||
<BasePageHeader :title="pageTitle.invoice_number">
|
||||
<template #actions>
|
||||
<BaseButton
|
||||
:disabled="isSendingEmail"
|
||||
variant="primary-outline"
|
||||
class="mr-2"
|
||||
tag="a"
|
||||
:href="`/invoices/pdf/${invoice.unique_hash}`"
|
||||
download
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="DownloadIcon" :class="slotProps.class" />
|
||||
{{ $t('invoices.download') }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
v-if="
|
||||
invoiceStore?.selectedViewInvoice?.paid_status !== 'PAID' &&
|
||||
globalStore.enabledModules.includes('Payments')
|
||||
"
|
||||
variant="primary"
|
||||
@click="payInvoice"
|
||||
>
|
||||
{{ $t('invoices.pay_invoice') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 bg-white w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
px-4
|
||||
pt-8
|
||||
pb-6
|
||||
border border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="searchData.invoice_number"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon name="SearchIcon" class="h-5 text-gray-400" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
|
||||
<div class="flex ml-3" role="group" aria-label="First group">
|
||||
<BaseDropdown
|
||||
position="bottom-start"
|
||||
width-class="w-50"
|
||||
position-class="left-0"
|
||||
>
|
||||
<template #activator>
|
||||
<BaseButton variant="gray">
|
||||
<BaseIcon name="FilterIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="
|
||||
px-4
|
||||
py-1
|
||||
pb-2
|
||||
mb-2
|
||||
text-sm
|
||||
border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="pt-3 rounded-md hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.invoice_date')"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="invoice_date"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="pt-3 rounded-md hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_due_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.due_date')"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="due_date"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="pt-3 rounded-md hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.invoice_number')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="invoice_number"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
</BaseDropdown>
|
||||
|
||||
<BaseButton class="ml-1" variant="white" @click="sortData">
|
||||
<BaseIcon v-if="getOrderBy" name="SortAscendingIcon" class="h-5" />
|
||||
<BaseIcon v-else name="SortDescendingIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
h-full
|
||||
pb-32
|
||||
overflow-y-scroll
|
||||
border-l border-gray-200 border-solid
|
||||
sw-scroll
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
v-for="(invoice, index) in invoiceStore.invoices"
|
||||
:id="'invoice-' + invoice.id"
|
||||
:key="index"
|
||||
:to="`/${globalStore.companySlug}/customer/invoices/${invoice.id}/view`"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid':
|
||||
hasActiveUrl(invoice.id),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
class="
|
||||
mb-1
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-gray-500
|
||||
capitalize
|
||||
text-md
|
||||
"
|
||||
>
|
||||
{{ invoice.invoice_number }}
|
||||
</div>
|
||||
<BaseInvoiceStatusBadge :status="invoice.status">
|
||||
{{ invoice.status }}
|
||||
</BaseInvoiceStatusBadge>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 whitespace-nowrap right">
|
||||
<BaseFormatMoney
|
||||
class="
|
||||
mb-2
|
||||
text-xl
|
||||
not-italic
|
||||
font-semibold
|
||||
leading-8
|
||||
text-right text-gray-900
|
||||
block
|
||||
"
|
||||
:amount="invoice.total"
|
||||
:currency="invoice.currency"
|
||||
/>
|
||||
|
||||
<div class="text-sm text-right text-gray-500 non-italic">
|
||||
{{ invoice.formatted_invoice_date }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<p
|
||||
v-if="!invoiceStore.invoices.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('invoices.no_matching_invoices') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- pdf -->
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
v-if="shareableLink"
|
||||
ref="report"
|
||||
:src="shareableLink"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md"
|
||||
@click="ViewReportsPDF"
|
||||
/>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue'
|
||||
import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { ref, reactive, computed, inject, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import moment from 'moment'
|
||||
import { useInvoiceStore } from '@/scripts/customer/stores/invoice'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
|
||||
// Router
|
||||
const route = useRoute()
|
||||
//store
|
||||
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const { tm } = useI18n()
|
||||
|
||||
//local state
|
||||
let invoice = reactive({})
|
||||
let searchData = reactive({
|
||||
orderBy: '',
|
||||
orderByField: '',
|
||||
invoice_number: '',
|
||||
// searchText: '',
|
||||
})
|
||||
|
||||
let url = ref(null)
|
||||
let siteURL = ref(null)
|
||||
let isSearching = ref(false)
|
||||
let isSendingEmail = ref(false)
|
||||
let isMarkingAsSent = ref(false)
|
||||
|
||||
//Utils
|
||||
const utils = inject('utils')
|
||||
|
||||
//Store
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// Computed Props
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
return invoiceStore.selectedViewInvoice
|
||||
})
|
||||
|
||||
const getOrderBy = computed(() => {
|
||||
if (searchData.orderBy === 'asc' || searchData.orderBy == null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const getOrderName = computed(() =>
|
||||
getOrderBy.value ? tm('general.ascending') : tm('general.descending')
|
||||
)
|
||||
|
||||
const shareableLink = computed(() => {
|
||||
return invoice.unique_hash ? `/invoices/pdf/${invoice.unique_hash}` : false
|
||||
})
|
||||
|
||||
// Watcher
|
||||
|
||||
watch(route, () => {
|
||||
loadInvoice()
|
||||
})
|
||||
|
||||
// Created
|
||||
|
||||
loadInvoices()
|
||||
loadInvoice()
|
||||
|
||||
onSearch = debounce(onSearch, 500)
|
||||
|
||||
// Methods
|
||||
|
||||
function hasActiveUrl(id) {
|
||||
return route.params.id == id
|
||||
}
|
||||
|
||||
async function loadInvoices() {
|
||||
await invoiceStore.fetchInvoices(
|
||||
{
|
||||
limit: 'all',
|
||||
},
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToInvoice()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function loadInvoice() {
|
||||
if (route && route.params.id) {
|
||||
let response = await invoiceStore.fetchViewInvoice(
|
||||
{
|
||||
id: route.params.id,
|
||||
},
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
if (response.data) {
|
||||
Object.assign(invoice, response.data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToInvoice() {
|
||||
const el = document.getElementById(`invoice-${route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
}
|
||||
|
||||
async function onSearch() {
|
||||
let data = {}
|
||||
|
||||
if (
|
||||
searchData.invoice_number !== '' &&
|
||||
searchData.invoice_number !== null &&
|
||||
searchData.invoice_number !== undefined
|
||||
) {
|
||||
data.invoice_number = searchData.invoice_number
|
||||
}
|
||||
|
||||
if (searchData.orderBy !== null && searchData.orderBy !== undefined) {
|
||||
data.orderBy = searchData.orderBy
|
||||
}
|
||||
|
||||
if (
|
||||
searchData.orderByField !== null &&
|
||||
searchData.orderByField !== undefined
|
||||
) {
|
||||
data.orderByField = searchData.orderByField
|
||||
}
|
||||
|
||||
isSearching.value = true
|
||||
try {
|
||||
let response = await invoiceStore.searchInvoice(
|
||||
data,
|
||||
globalStore.companySlug
|
||||
)
|
||||
isSearching.value = false
|
||||
|
||||
if (response.data.data) {
|
||||
invoiceStore.invoices = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function sortData() {
|
||||
if (searchData.orderBy === 'asc') {
|
||||
searchData.orderBy = 'desc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
searchData.orderBy = 'asc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
function payInvoice() {
|
||||
router.push({
|
||||
name: 'invoice.portal.payment',
|
||||
params: {
|
||||
id: invoiceStore.selectedViewInvoice.id,
|
||||
company: invoiceStore.selectedViewInvoice.company.slug,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
259
resources/scripts/customer/views/payments/Index.vue
Normal file
259
resources/scripts/customer/views/payments/Index.vue
Normal file
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<BasePageHeader :title="$t('payments.title')">
|
||||
<BaseBreadcrumb slot="breadcrumbs">
|
||||
<BaseBreadcrumbItem
|
||||
:title="$t('general.home')"
|
||||
:to="`/${globalStore.companySlug}/customer/dashboard`"
|
||||
/>
|
||||
|
||||
<BaseBreadcrumbItem :title="$tc('payments.payment', 2)" to="#" active />
|
||||
</BaseBreadcrumb>
|
||||
|
||||
<template #actions>
|
||||
<BaseButton
|
||||
v-show="paymentStore.totalPayments"
|
||||
variant="primary-outline"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('general.filter') }}
|
||||
<template #right="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!showFilters"
|
||||
:class="slotProps.class"
|
||||
name="FilterIcon"
|
||||
/>
|
||||
<BaseIcon v-else :class="slotProps.class" name="XIcon" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<BaseFilterWrapper v-show="showFilters" @clear="clearFilter">
|
||||
<BaseInputGroup :label="$t('payments.payment_number')" class="px-3">
|
||||
<BaseInput
|
||||
v-model="filters.payment_number"
|
||||
:placeholder="$t('payments.payment_number')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('payments.payment_mode')" class="px-3">
|
||||
<BaseMultiselect
|
||||
v-model="filters.payment_mode"
|
||||
value-prop="id"
|
||||
track-by="name"
|
||||
:filter-results="false"
|
||||
label="name"
|
||||
resolve-on-load
|
||||
:delay="100"
|
||||
searchable
|
||||
:options="searchPayment"
|
||||
:placeholder="$t('payments.payment_mode')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseFilterWrapper>
|
||||
|
||||
<BaseEmptyPlaceholder
|
||||
v-if="showEmptyScreen"
|
||||
:title="$t('payments.no_payments')"
|
||||
:description="$t('payments.list_of_payments')"
|
||||
>
|
||||
<CapsuleIcon class="mt-5 mb-4" />
|
||||
</BaseEmptyPlaceholder>
|
||||
|
||||
<div v-show="!showEmptyScreen" class="relative table-container">
|
||||
<BaseTable
|
||||
ref="table"
|
||||
:data="fetchData"
|
||||
:columns="paymentColumns"
|
||||
:placeholder-count="paymentStore.totalPayments >= 20 ? 10 : 5"
|
||||
class="mt-10"
|
||||
>
|
||||
<template #cell-payment_date="{ row }">
|
||||
{{ row.data.formatted_payment_date }}
|
||||
</template>
|
||||
|
||||
<template #cell-payment_number="{ row }">
|
||||
<router-link
|
||||
:to="{
|
||||
path: `payments/${row.data.id}/view`,
|
||||
}"
|
||||
class="font-medium text-primary-500"
|
||||
>
|
||||
{{ row.data.payment_number }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template #cell-payment_mode="{ row }">
|
||||
<span>
|
||||
{{
|
||||
row.data.payment_method
|
||||
? row.data.payment_method.name
|
||||
: $t('payments.not_selected')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-invoice_number="{ row }">
|
||||
<span>
|
||||
{{
|
||||
row.data.invoice?.invoice_number
|
||||
? row.data.invoice?.invoice_number
|
||||
: $t('payments.no_invoice')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-amount="{ row }">
|
||||
<div v-html="utils.formatMoney(row.data.amount, currency)" />
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="w-5 text-gray-500" />
|
||||
</template>
|
||||
<router-link :to="`payments/${row.data.id}/view`">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon name="EyeIcon" class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { debouncedWatch } from '@vueuse/core'
|
||||
import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue'
|
||||
import CapsuleIcon from '@/scripts/components/icons/empty/CapsuleIcon.vue'
|
||||
import { ref, reactive, inject, computed } from 'vue'
|
||||
import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue'
|
||||
import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/scripts/customer/stores/payment'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { tm, t } = useI18n()
|
||||
let showFilters = ref(false)
|
||||
let sortedBy = ref('created_at')
|
||||
let isFetchingInitialData = ref(true)
|
||||
let table = ref(null)
|
||||
const filters = reactive({
|
||||
payment_mode: '',
|
||||
payment_number: '',
|
||||
})
|
||||
|
||||
//Utils
|
||||
const utils = inject('utils')
|
||||
|
||||
//Store
|
||||
const route = useRoute()
|
||||
const paymentStore = usePaymentStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// Computed Props
|
||||
|
||||
const showEmptyScreen = computed(() => {
|
||||
return !paymentStore.totalPayments && !isFetchingInitialData.value
|
||||
})
|
||||
|
||||
const currency = computed(() => {
|
||||
return globalStore.currency
|
||||
})
|
||||
|
||||
// Payment Table columns Data
|
||||
|
||||
const paymentColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'payment_date',
|
||||
label: t('payments.date'),
|
||||
thClass: 'extra',
|
||||
tdClass: 'font-medium text-gray-900',
|
||||
},
|
||||
{ key: 'payment_number', label: t('payments.payment_number') },
|
||||
{ key: 'payment_mode', label: t('payments.payment_mode') },
|
||||
{ key: 'invoice_number', label: t('invoices.invoice_number') },
|
||||
{ key: 'amount', label: t('payments.amount') },
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'text-right text-sm font-medium',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// Created
|
||||
|
||||
debouncedWatch(
|
||||
filters,
|
||||
() => {
|
||||
setFilters()
|
||||
},
|
||||
{ debounce: 500 }
|
||||
)
|
||||
|
||||
// Methods
|
||||
|
||||
async function searchPayment(search) {
|
||||
let res = await paymentStore.fetchPaymentModes(
|
||||
search,
|
||||
globalStore.companySlug
|
||||
)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
async function fetchData({ page, filter, sort }) {
|
||||
let data = {
|
||||
payment_method_id:
|
||||
filters.payment_mode !== null ? filters.payment_mode : '',
|
||||
payment_number: filters.payment_number,
|
||||
orderByField: sort.fieldName || 'created_at',
|
||||
orderBy: sort.order || 'desc',
|
||||
page,
|
||||
}
|
||||
|
||||
isFetchingInitialData.value = true
|
||||
let response = await paymentStore.fetchPayments(data, globalStore.companySlug)
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
pagination: {
|
||||
totalPages: response.data.meta.last_page,
|
||||
currentPage: page,
|
||||
totalCount: response.data.meta.total,
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTable() {
|
||||
table.value.refresh()
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
refreshTable()
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
filters.customer = ''
|
||||
filters.payment_mode = ''
|
||||
filters.payment_number = ''
|
||||
}
|
||||
|
||||
function toggleFilter() {
|
||||
if (showFilters.value) {
|
||||
clearFilter()
|
||||
}
|
||||
|
||||
showFilters.value = !showFilters.value
|
||||
}
|
||||
</script>
|
||||
374
resources/scripts/customer/views/payments/View.vue
Normal file
374
resources/scripts/customer/views/payments/View.vue
Normal file
@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<BasePage class="xl:pl-96">
|
||||
<BasePageHeader :title="pageTitle.payment_number">
|
||||
<template #actions>
|
||||
<BaseButton
|
||||
:disabled="isSendingEmail"
|
||||
variant="primary-outline"
|
||||
tag="a"
|
||||
download
|
||||
:href="`/payments/pdf/${payment.unique_hash}`"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="DownloadIcon" :class="slotProps.class" />
|
||||
{{ $t('general.download') }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-4 bg-white w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
px-4
|
||||
pt-8
|
||||
pb-6
|
||||
border border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="searchData.payment_number"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon name="SearchIcon" class="h-5 text-gray-400" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
|
||||
<div class="flex ml-3" role="group" aria-label="First group">
|
||||
<BaseDropdown
|
||||
position="bottom-start"
|
||||
width-class="w-50"
|
||||
position-class="left-0"
|
||||
>
|
||||
<template #activator>
|
||||
<BaseButton variant="gray">
|
||||
<BaseIcon name="FilterIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="
|
||||
px-4
|
||||
py-1
|
||||
pb-2
|
||||
mb-2
|
||||
text-sm
|
||||
border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.title')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="invoice_number"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_payment_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('payments.date')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="payment_date"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
|
||||
<div class="px-2">
|
||||
<BaseDropdownItem class="rounded-md pt-3 hover:rounded-md">
|
||||
<BaseInputGroup class="-mt-3 font-normal">
|
||||
<BaseRadio
|
||||
id="filter_payment_number"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('payments.payment_number')"
|
||||
size="sm"
|
||||
name="filter"
|
||||
value="payment_number"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseDropdownItem>
|
||||
</div>
|
||||
</BaseDropdown>
|
||||
|
||||
<BaseButton class="ml-1" variant="white" @click="sortData">
|
||||
<BaseIcon v-if="getOrderBy" name="SortAscendingIcon" class="h-5" />
|
||||
<BaseIcon v-else name="SortDescendingIcon" class="h-5" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
h-full
|
||||
pb-32
|
||||
overflow-y-scroll
|
||||
border-l border-gray-200 border-solid
|
||||
sw-scroll
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
v-for="(payment, index) in paymentStore.payments"
|
||||
:id="'payment-' + payment.id"
|
||||
:key="index"
|
||||
:to="`/${globalStore.companySlug}/customer/payments/${payment.id}/view`"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid':
|
||||
hasActiveUrl(payment.id),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
class="
|
||||
mb-1
|
||||
text-md
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-gray-500
|
||||
capitalize
|
||||
"
|
||||
>
|
||||
{{ payment.payment_number }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 whitespace-nowrap right">
|
||||
<BaseFormatMoney
|
||||
class="
|
||||
mb-2
|
||||
text-xl
|
||||
not-italic
|
||||
font-semibold
|
||||
leading-8
|
||||
text-right text-gray-900
|
||||
block
|
||||
"
|
||||
:amount="payment.amount"
|
||||
:currency="payment.currency"
|
||||
/>
|
||||
|
||||
<div class="text-sm text-right text-gray-500 non-italic">
|
||||
{{ payment.formatted_payment_date }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<p
|
||||
v-if="!paymentStore.payments.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('payments.no_matching_payments') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- pdf -->
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
v-if="shareableLink"
|
||||
:src="shareableLink"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue'
|
||||
import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { ref, reactive, computed, inject, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import moment from 'moment'
|
||||
import { usePaymentStore } from '@/scripts/customer/stores/payment'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
|
||||
// Router
|
||||
const route = useRoute()
|
||||
const paymentStore = usePaymentStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const { tm, t } = useI18n()
|
||||
// let id = ref(null)
|
||||
|
||||
let payment = reactive({})
|
||||
let searchData = reactive({
|
||||
orderBy: '',
|
||||
orderByField: '',
|
||||
payment_number: '',
|
||||
})
|
||||
|
||||
let isSearching = ref(false)
|
||||
let isSendingEmail = ref(false)
|
||||
let isMarkingAsSent = ref(false)
|
||||
|
||||
//Utils
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
//Store
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
// Computed Props
|
||||
const pageTitle = computed(() => {
|
||||
return paymentStore.selectedViewPayment
|
||||
})
|
||||
|
||||
const getOrderBy = computed(() => {
|
||||
if (searchData.orderBy === 'asc' || searchData.orderBy == null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const getOrderName = computed(() =>
|
||||
getOrderBy.value ? tm('general.ascending') : tm('general.descending')
|
||||
)
|
||||
|
||||
const shareableLink = computed(() => {
|
||||
return payment.unique_hash ? `/payments/pdf/${payment.unique_hash}` : false
|
||||
})
|
||||
|
||||
// Watcher
|
||||
|
||||
watch(route, () => {
|
||||
loadPayment()
|
||||
})
|
||||
|
||||
// Created
|
||||
|
||||
loadPayments()
|
||||
loadPayment()
|
||||
|
||||
onSearch = debounce(onSearch, 500)
|
||||
|
||||
// Methods
|
||||
|
||||
function hasActiveUrl(id) {
|
||||
return route.params.id == id
|
||||
}
|
||||
|
||||
async function loadPayments() {
|
||||
await paymentStore.fetchPayments(
|
||||
{
|
||||
limit: 'all',
|
||||
},
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToPayment()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function loadPayment() {
|
||||
if (route && route.params.id) {
|
||||
let response = await paymentStore.fetchViewPayment(
|
||||
{
|
||||
id: route.params.id,
|
||||
},
|
||||
globalStore.companySlug
|
||||
)
|
||||
|
||||
if (response.data) {
|
||||
Object.assign(payment, response.data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToPayment() {
|
||||
const el = document.getElementById(`payment-${route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
}
|
||||
|
||||
async function onSearch() {
|
||||
let data = {}
|
||||
|
||||
if (
|
||||
searchData.payment_number !== '' &&
|
||||
searchData.payment_number !== null &&
|
||||
searchData.payment_number !== undefined
|
||||
) {
|
||||
data.payment_number = searchData.payment_number
|
||||
}
|
||||
|
||||
if (searchData.orderBy !== null && searchData.orderBy !== undefined) {
|
||||
data.orderBy = searchData.orderBy
|
||||
}
|
||||
|
||||
if (
|
||||
searchData.orderByField !== null &&
|
||||
searchData.orderByField !== undefined
|
||||
) {
|
||||
data.orderByField = searchData.orderByField
|
||||
}
|
||||
|
||||
isSearching.value = true
|
||||
try {
|
||||
let response = await paymentStore.searchPayment(
|
||||
data,
|
||||
globalStore.companySlug
|
||||
)
|
||||
isSearching.value = false
|
||||
|
||||
if (response.data.data) {
|
||||
paymentStore.payments = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function sortData() {
|
||||
if (searchData.orderBy === 'asc') {
|
||||
searchData.orderBy = 'desc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
searchData.orderBy = 'asc'
|
||||
onSearch()
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
282
resources/scripts/customer/views/settings/AddressInformation.vue
Normal file
282
resources/scripts/customer/views/settings/AddressInformation.vue
Normal file
@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<form class="relative h-full mt-4" @submit.prevent="UpdateCustomerAddress">
|
||||
<BaseCard>
|
||||
<div class="mb-6">
|
||||
<h6 class="font-bold text-left">
|
||||
{{ $t('settings.menu_title.address_information') }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-left text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{ $t('settings.address_information.section_description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Billing Address -->
|
||||
|
||||
<div class="grid grid-cols-5 gap-4 mb-8">
|
||||
<h6 class="col-span-5 text-lg font-semibold text-left lg:col-span-1">
|
||||
{{ $t('customers.billing_address') }}
|
||||
</h6>
|
||||
|
||||
<div
|
||||
class="grid col-span-5 lg:col-span-4 gap-y-6 gap-x-4 md:grid-cols-6"
|
||||
>
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.name')"
|
||||
class="w-full md:col-span-3"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.billing.name"
|
||||
type="text"
|
||||
class="w-full"
|
||||
name="address_name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.country')"
|
||||
class="md:col-span-3"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="userStore.userForm.billing.country_id"
|
||||
value-prop="id"
|
||||
label="name"
|
||||
track-by="name"
|
||||
resolve-on-load
|
||||
searchable
|
||||
:options="globalStore.countries"
|
||||
:placeholder="$t('general.select_country')"
|
||||
class="w-full"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')" class="md:col-span-3">
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.billing.state"
|
||||
name="billing.state"
|
||||
type="text"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')" class="md:col-span-3">
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.billing.city"
|
||||
name="billing.city"
|
||||
type="text"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
class="md:col-span-3"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model.trim="userStore.userForm.billing.address_street_1"
|
||||
:placeholder="$t('general.street_1')"
|
||||
type="text"
|
||||
name="billing_street1"
|
||||
:container-class="`mt-3`"
|
||||
/>
|
||||
|
||||
<BaseTextarea
|
||||
v-model.trim="userStore.userForm.billing.address_street_2"
|
||||
:placeholder="$t('general.street_2')"
|
||||
type="text"
|
||||
class="mt-3"
|
||||
name="billing_street2"
|
||||
:container-class="`mt-3`"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<BaseInputGroup :label="$t('customers.phone')" class="text-left">
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.billing.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.zip_code')"
|
||||
class="mt-2 text-left"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.billing.zip"
|
||||
type="text"
|
||||
name="zip"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseDivider class="mb-5 md:mb-8" />
|
||||
|
||||
<!-- Billing Address Copy Button -->
|
||||
<div class="flex items-center justify-start mb-6 md:justify-end md:mb-0">
|
||||
<div class="p-1">
|
||||
<BaseButton
|
||||
ref="sameAddress"
|
||||
type="button"
|
||||
@click="userStore.copyAddress(true)"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="DocumentDuplicateIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('customers.copy_billing_address') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address -->
|
||||
|
||||
<div class="grid grid-cols-5 gap-4 mb-8">
|
||||
<h6 class="col-span-5 text-lg font-semibold text-left lg:col-span-1">
|
||||
{{ $t('customers.shipping_address') }}
|
||||
</h6>
|
||||
|
||||
<div
|
||||
v-if="userStore.userForm.shipping"
|
||||
class="grid col-span-5 lg:col-span-4 gap-y-6 gap-x-4 md:grid-cols-6"
|
||||
>
|
||||
<BaseInputGroup :label="$t('customers.name')" class="md:col-span-3">
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.shipping.name"
|
||||
type="text"
|
||||
name="address_name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.country')"
|
||||
class="md:col-span-3"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="userStore.userForm.shipping.country_id"
|
||||
value-prop="id"
|
||||
label="name"
|
||||
track-by="name"
|
||||
resolve-on-load
|
||||
searchable
|
||||
:options="globalStore.countries"
|
||||
:placeholder="$t('general.select_country')"
|
||||
class="w-full"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')" class="md:col-span-3">
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.shipping.state"
|
||||
name="shipping.state"
|
||||
type="text"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')" class="md:col-span-3">
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.shipping.city"
|
||||
name="shipping.city"
|
||||
type="text"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
class="md:col-span-3"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model.trim="userStore.userForm.shipping.address_street_1"
|
||||
type="text"
|
||||
:placeholder="$t('general.street_1')"
|
||||
name="shipping_street1"
|
||||
/>
|
||||
|
||||
<BaseTextarea
|
||||
v-model.trim="userStore.userForm.shipping.address_street_2"
|
||||
type="text"
|
||||
:placeholder="$t('general.street_2')"
|
||||
name="shipping_street2"
|
||||
class="mt-3"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<BaseInputGroup :label="$t('customers.phone')" class="text-left">
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.shipping.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.zip_code')"
|
||||
class="mt-2 text-left"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="userStore.userForm.shipping.zip"
|
||||
type="text"
|
||||
name="zip"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<BaseButton :loading="isSaving" :disabled="isSaving">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/scripts/customer/stores/user'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
//store
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const { tm, t } = useI18n()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// local state
|
||||
let isSaving = ref(false)
|
||||
|
||||
// created
|
||||
|
||||
globalStore.fetchCountries()
|
||||
|
||||
// methods
|
||||
|
||||
function UpdateCustomerAddress() {
|
||||
isSaving.value = true
|
||||
let data = userStore.userForm
|
||||
userStore
|
||||
.updateCurrentUser({
|
||||
data,
|
||||
message: tm('customers.address_updated_message'),
|
||||
})
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
252
resources/scripts/customer/views/settings/CustomerSettings.vue
Normal file
252
resources/scripts/customer/views/settings/CustomerSettings.vue
Normal file
@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<form class="relative h-full mt-4" @submit.prevent="updateCustomerData">
|
||||
<BaseCard>
|
||||
<div>
|
||||
<h6 class="font-bold text-left">
|
||||
{{ $t('settings.account_settings.account_settings') }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-left text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{ $t('settings.account_settings.section_description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-col-1 md:grid-cols-2 mt-6">
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.profile_picture')"
|
||||
>
|
||||
<BaseFileUploader
|
||||
v-model="imgFiles"
|
||||
:avatar="true"
|
||||
accept="image/*"
|
||||
@change="onFileInputChange"
|
||||
@remove="onFileInputRemove"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<!-- Empty Column -->
|
||||
<span></span>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.name')"
|
||||
:error="
|
||||
v$.userForm.name.$error && v$.userForm.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.name"
|
||||
:invalid="v$.userForm.name.$error"
|
||||
@input="v$.userForm.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.email')"
|
||||
:error="
|
||||
v$.userForm.email.$error && v$.userForm.email.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.email"
|
||||
:invalid="v$.userForm.email.$error"
|
||||
@input="v$.userForm.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.userForm.password.$error &&
|
||||
v$.userForm.password.$errors[0].$message
|
||||
"
|
||||
:label="$tc('settings.account_settings.password')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.password"
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
:invalid="v$.userForm.password.$error"
|
||||
@input="v$.userForm.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
name="EyeOffIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/> </template
|
||||
></BaseInput>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.account_settings.confirm_password')"
|
||||
:error="
|
||||
v$.userForm.confirm_password.$error &&
|
||||
v$.userForm.confirm_password.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="userStore.userForm.confirm_password"
|
||||
:type="isShowConfirmPassword ? 'text' : 'password'"
|
||||
:invalid="v$.userForm.confirm_password.$error"
|
||||
@input="v$.userForm.confirm_password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowConfirmPassword"
|
||||
name="EyeOffIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowConfirmPassword = !isShowConfirmPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowConfirmPassword = !isShowConfirmPassword"
|
||||
/> </template
|
||||
></BaseInput>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<BaseButton :loading="isSaving" :disabled="isSaving" class="mt-6">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</BaseCard>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { SaveIcon } from '@heroicons/vue/solid'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useUserStore } from '@/scripts/customer/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
helpers,
|
||||
sameAs,
|
||||
email,
|
||||
required,
|
||||
minLength,
|
||||
} from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const route = useRoute()
|
||||
const { t, tm } = useI18n()
|
||||
|
||||
// Local State
|
||||
let imgFiles = ref([])
|
||||
let isSaving = ref(false)
|
||||
let avatarFileBlob = ref(null)
|
||||
let isShowPassword = ref(false)
|
||||
let isShowConfirmPassword = ref(false)
|
||||
|
||||
if (userStore.userForm.avatar) {
|
||||
imgFiles.value.push({
|
||||
image: userStore.userForm.avatar,
|
||||
})
|
||||
}
|
||||
|
||||
// Validation
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
userForm: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
email: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
password: {
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.password_min_length', { count: 8 }),
|
||||
minLength(8)
|
||||
),
|
||||
},
|
||||
confirm_password: {
|
||||
sameAsPassword: helpers.withMessage(
|
||||
t('validation.password_incorrect'),
|
||||
sameAs(userStore.userForm.password)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => userStore)
|
||||
)
|
||||
|
||||
// created
|
||||
|
||||
// methods
|
||||
|
||||
function onFileInputChange(fileName, file) {
|
||||
avatarFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
avatarFileBlob.value = null
|
||||
}
|
||||
|
||||
function updateCustomerData() {
|
||||
v$.value.userForm.$touch()
|
||||
|
||||
if (v$.value.userForm.$invalid) {
|
||||
return true
|
||||
}
|
||||
isSaving.value = true
|
||||
|
||||
let data = new FormData()
|
||||
data.append('name', userStore.userForm.name)
|
||||
data.append('email', userStore.userForm.email)
|
||||
|
||||
if (
|
||||
userStore.userForm.password != null &&
|
||||
userStore.userForm.password !== undefined &&
|
||||
userStore.userForm.password !== ''
|
||||
) {
|
||||
data.append('password', userStore.userForm.password)
|
||||
}
|
||||
if (avatarFileBlob.value) {
|
||||
data.append('customer_avatar', avatarFileBlob.value)
|
||||
}
|
||||
|
||||
userStore
|
||||
.updateCurrentUser({
|
||||
data,
|
||||
message: tm('settings.account_settings.updated_message'),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.data) {
|
||||
isSaving.value = false
|
||||
userStore.$patch((state) => {
|
||||
state.userForm.password = ''
|
||||
state.userForm.confirm_password = ''
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
130
resources/scripts/customer/views/settings/SettingsIndex.vue
Normal file
130
resources/scripts/customer/views/settings/SettingsIndex.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<BasePageHeader :title="$tc('settings.setting', 2)" class="pb-6">
|
||||
<BaseBreadcrumb>
|
||||
<BaseBreadcrumbItem
|
||||
:title="$t('general.home')"
|
||||
:to="`/${companySlug}/customer/dashboard`"
|
||||
/>
|
||||
<BaseBreadcrumbItem
|
||||
:title="$tc('settings.setting', 2)"
|
||||
:to="`/${companySlug}/customer/settings/customer-profile`"
|
||||
active
|
||||
/>
|
||||
</BaseBreadcrumb>
|
||||
</BasePageHeader>
|
||||
|
||||
<div class="w-full mb-6 select-wrapper xl:hidden">
|
||||
<aside class="pb-3 lg:col-span-3">
|
||||
<nav class="space-y-1">
|
||||
<BaseList>
|
||||
<BaseListItem
|
||||
v-for="(menuItem, index) in menuItems"
|
||||
:key="index"
|
||||
:title="menuItem.title"
|
||||
:to="menuItem.link"
|
||||
:active="hasActiveUrl(menuItem.link)"
|
||||
:index="index"
|
||||
class="py-3"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="menuItem.icon" class="h-5 w-6" />
|
||||
</template>
|
||||
</BaseListItem>
|
||||
</BaseList>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="hidden mt-1 xl:block min-w-[240px]">
|
||||
<BaseList>
|
||||
<BaseListItem
|
||||
v-for="(menuItem, index) in menuItems"
|
||||
:key="index"
|
||||
:title="menuItem.title"
|
||||
:to="menuItem.link"
|
||||
:active="hasActiveUrl(menuItem.link)"
|
||||
:index="index"
|
||||
class="py-3"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="menuItem.icon" class="h-5 w-6" />
|
||||
</template>
|
||||
</BaseListItem>
|
||||
</BaseList>
|
||||
</div>
|
||||
<div class="w-full overflow-hidden">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watchEffect, computed } from 'vue'
|
||||
import BaseList from '@/scripts/components/list/BaseList.vue'
|
||||
import BaseListItem from '@/scripts/components/list/BaseListItem.vue'
|
||||
import { OfficeBuildingIcon, UserIcon } from '@heroicons/vue/outline'
|
||||
import { useGlobalStore } from '@/scripts/customer/stores/global'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
//route
|
||||
const { useRoute, useRouter } = window.VueRouter
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const companySlug = computed(() => {
|
||||
return globalStore.companySlug
|
||||
})
|
||||
|
||||
// local state
|
||||
const { global } = window.i18n
|
||||
let currentSetting = ref({})
|
||||
let activeIndex = ref()
|
||||
const menuItems = reactive([
|
||||
{
|
||||
link: `/${globalStore.companySlug}/customer/settings/customer-profile`,
|
||||
title: t('settings.account_settings.account_settings'),
|
||||
icon: UserIcon,
|
||||
},
|
||||
{
|
||||
link: `/${globalStore.companySlug}/customer/settings/address-info`,
|
||||
title: t('settings.menu_title.address_information'),
|
||||
icon: OfficeBuildingIcon,
|
||||
},
|
||||
])
|
||||
|
||||
// watch
|
||||
|
||||
watchEffect(() => {
|
||||
if (route.path === `/${globalStore.companySlug}/customer/settings`) {
|
||||
router.push({ name: 'customer.profile' })
|
||||
}
|
||||
|
||||
const menu = menuItems.find((item) => {
|
||||
return item.link === route.path
|
||||
})
|
||||
|
||||
currentSetting.value = { ...menu }
|
||||
})
|
||||
|
||||
// computed props
|
||||
|
||||
const dropdownMenuItems = computed(() => {
|
||||
return menuItems
|
||||
})
|
||||
|
||||
// methods
|
||||
|
||||
function hasActiveUrl(url) {
|
||||
return route.path.indexOf(url) > -1
|
||||
}
|
||||
|
||||
function navigateToSetting(setting) {
|
||||
return router.push(setting.link)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user