mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 19:51:09 -04:00
v6 update
This commit is contained in:
98
resources/scripts/admin/components/CopyInputField.vue
Normal file
98
resources/scripts/admin/components/CopyInputField.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
relative
|
||||
flex
|
||||
px-4
|
||||
py-2
|
||||
rounded-lg
|
||||
bg-opacity-40 bg-gray-300
|
||||
whitespace-nowrap
|
||||
flex-col
|
||||
mt-1
|
||||
"
|
||||
>
|
||||
<span
|
||||
ref="publicUrl"
|
||||
class="
|
||||
pr-10
|
||||
text-sm
|
||||
font-medium
|
||||
text-black
|
||||
truncate
|
||||
select-all select-color
|
||||
"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
<svg
|
||||
v-tooltip="{ content: 'Copy to Clipboard' }"
|
||||
class="
|
||||
absolute
|
||||
right-0
|
||||
h-full
|
||||
inset-y-0
|
||||
cursor-pointer
|
||||
focus:outline-none
|
||||
text-primary-500
|
||||
"
|
||||
width="37"
|
||||
viewBox="0 0 37 37"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click="copyUrl"
|
||||
>
|
||||
<rect width="37" height="37" rx="10" fill="currentColor" />
|
||||
<path
|
||||
d="M16 10C15.7348 10 15.4804 10.1054 15.2929 10.2929C15.1054 10.4804 15 10.7348 15 11C15 11.2652 15.1054 11.5196 15.2929 11.7071C15.4804 11.8946 15.7348 12 16 12H18C18.2652 12 18.5196 11.8946 18.7071 11.7071C18.8946 11.5196 19 11.2652 19 11C19 10.7348 18.8946 10.4804 18.7071 10.2929C18.5196 10.1054 18.2652 10 18 10H16Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M11 13C11 12.4696 11.2107 11.9609 11.5858 11.5858C11.9609 11.2107 12.4696 11 13 11C13 11.7956 13.3161 12.5587 13.8787 13.1213C14.4413 13.6839 15.2044 14 16 14H18C18.7956 14 19.5587 13.6839 20.1213 13.1213C20.6839 12.5587 21 11.7956 21 11C21.5304 11 22.0391 11.2107 22.4142 11.5858C22.7893 11.9609 23 12.4696 23 13V19H18.414L19.707 17.707C19.8892 17.5184 19.99 17.2658 19.9877 17.0036C19.9854 16.7414 19.8802 16.4906 19.6948 16.3052C19.5094 16.1198 19.2586 16.0146 18.9964 16.0123C18.7342 16.01 18.4816 16.1108 18.293 16.293L15.293 19.293C15.1055 19.4805 15.0002 19.7348 15.0002 20C15.0002 20.2652 15.1055 20.5195 15.293 20.707L18.293 23.707C18.4816 23.8892 18.7342 23.99 18.9964 23.9877C19.2586 23.9854 19.5094 23.8802 19.6948 23.6948C19.8802 23.5094 19.9854 23.2586 19.9877 22.9964C19.99 22.7342 19.8892 22.4816 19.707 22.293L18.414 21H23V24C23 24.5304 22.7893 25.0391 22.4142 25.4142C22.0391 25.7893 21.5304 26 21 26H13C12.4696 26 11.9609 25.7893 11.5858 25.4142C11.2107 25.0391 11 24.5304 11 24V13ZM23 19H25C25.2652 19 25.5196 19.1054 25.7071 19.2929C25.8946 19.4804 26 19.7348 26 20C26 20.2652 25.8946 20.5196 25.7071 20.7071C25.5196 20.8946 25.2652 21 25 21H23V19Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { ref } from 'vue'
|
||||
const notificationStore = useNotificationStore()
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const publicUrl = ref('')
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
token: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
function selectText(element) {
|
||||
let range
|
||||
if (document.selection) {
|
||||
// IE
|
||||
range = document.body.createTextRange()
|
||||
range.moveToElementText(element)
|
||||
range.select()
|
||||
} else if (window.getSelection) {
|
||||
range = document.createRange()
|
||||
range.selectNode(element)
|
||||
window.getSelection().removeAllRanges()
|
||||
window.getSelection().addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
selectText(publicUrl.value)
|
||||
document.execCommand('copy')
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
209
resources/scripts/admin/components/SelectNotePopup.vue
Normal file
209
resources/scripts/admin/components/SelectNotePopup.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<NoteModal />
|
||||
<div class="w-full">
|
||||
<Popover v-slot="{ isOpen }">
|
||||
<PopoverButton
|
||||
v-if="userStore.hasAbilities(abilities.VIEW_NOTE)"
|
||||
:class="isOpen ? '' : 'text-opacity-90'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
z-10
|
||||
font-medium
|
||||
text-primary-400
|
||||
focus:outline-none focus:border-none
|
||||
"
|
||||
@click="fetchInitialData"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PlusIcon"
|
||||
class="w-4 h-4 font-medium text-primary-400"
|
||||
/>
|
||||
{{ $t('general.insert_note') }}
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Note Select Popup -->
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
class="
|
||||
absolute
|
||||
z-20
|
||||
px-4
|
||||
mt-3
|
||||
sm:px-0
|
||||
w-screen
|
||||
max-w-full
|
||||
left-0
|
||||
top-3
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
shadow-lg
|
||||
ring-1 ring-black ring-opacity-5
|
||||
"
|
||||
>
|
||||
<div class="relative grid bg-white">
|
||||
<div class="relative p-4">
|
||||
<BaseInput
|
||||
v-model="textSearch"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
class="text-black"
|
||||
>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="filteredNotes.length > 0"
|
||||
class="relative flex flex-col overflow-auto list max-h-36"
|
||||
>
|
||||
<div
|
||||
v-for="(note, index) in filteredNotes"
|
||||
:key="index"
|
||||
tabindex="2"
|
||||
class="
|
||||
px-6
|
||||
py-4
|
||||
border-b border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:bg-gray-100 hover:cursor-pointer
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectNote(index, close)"
|
||||
>
|
||||
<div class="flex justify-between px-2">
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
leading-tight
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ note.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-center p-5 text-gray-400">
|
||||
<label class="text-base text-gray-500">
|
||||
{{ $t('general.no_note_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
type="button"
|
||||
class="
|
||||
h-10
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
py-3
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openNoteModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('settings.customization.notes.add_new_note') }}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
|
||||
const table = ref(null)
|
||||
const { t } = useI18n()
|
||||
const textSearch = ref(null)
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const noteStore = useNotesStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const filteredNotes = computed(() => {
|
||||
if (textSearch.value) {
|
||||
return noteStore.notes.filter(function (el) {
|
||||
return (
|
||||
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return noteStore.notes
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchInitialData() {
|
||||
await noteStore.fetchNotes({
|
||||
filter: {},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
type: props.type ? props.type : '',
|
||||
})
|
||||
}
|
||||
|
||||
function selectNote(data, close) {
|
||||
emit('select', { ...noteStore.notes[data] })
|
||||
|
||||
textSearch.value = null
|
||||
close()
|
||||
}
|
||||
|
||||
function openNoteModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.notes.add_note'),
|
||||
componentName: 'NoteModal',
|
||||
size: 'lg',
|
||||
data: props.type,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
197
resources/scripts/admin/components/charts/LineChart.vue
Normal file
197
resources/scripts/admin/components/charts/LineChart.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="graph-container h-[300px]">
|
||||
<canvas id="graph" ref="graph" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chart from 'chart.js'
|
||||
import { ref, reactive, computed, onMounted, watchEffect, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
invoices: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
expenses: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
receipts: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
income: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
let myLineChart = null
|
||||
const graph = ref(null)
|
||||
const companyStore = useCompanyStore()
|
||||
const defaultCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.labels) {
|
||||
if (myLineChart) {
|
||||
myLineChart.reset()
|
||||
update()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
let context = graph.value.getContext('2d')
|
||||
let options = reactive({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
return utils.formatMoney(
|
||||
Math.round(tooltipItem.value * 100),
|
||||
defaultCurrency.value
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
})
|
||||
|
||||
let data = reactive({
|
||||
labels: props.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Sales',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(230, 254, 249)',
|
||||
borderColor: '#040405',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: '#040405',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: '#040405',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.invoices.map((invoice) => invoice / 100),
|
||||
},
|
||||
{
|
||||
label: 'Receipts',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(230, 254, 249)',
|
||||
borderColor: 'rgb(2, 201, 156)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgb(2, 201, 156)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgb(2, 201, 156)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.receipts.map((receipt) => receipt / 100),
|
||||
},
|
||||
{
|
||||
label: 'Expenses',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(245, 235, 242)',
|
||||
borderColor: 'rgb(255,0,0)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgb(255,0,0)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgb(255,0,0)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.expenses.map((expense) => expense / 100),
|
||||
},
|
||||
{
|
||||
label: 'Net Income',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(236, 235, 249)',
|
||||
borderColor: 'rgba(88, 81, 216, 1)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgba(88, 81, 216, 1)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgba(88, 81, 216, 1)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.income.map((_i) => _i / 100),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
myLineChart = new Chart(context, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options,
|
||||
})
|
||||
})
|
||||
|
||||
function update() {
|
||||
myLineChart.data.labels = props.labels
|
||||
myLineChart.data.datasets[0].data = props.invoices.map(
|
||||
(invoice) => invoice / 100
|
||||
)
|
||||
myLineChart.data.datasets[1].data = props.receipts.map(
|
||||
(receipt) => receipt / 100
|
||||
)
|
||||
myLineChart.data.datasets[2].data = props.expenses.map(
|
||||
(expense) => expense / 100
|
||||
)
|
||||
myLineChart.data.datasets[3].data = props.income.map((_i) => _i / 100)
|
||||
myLineChart.update({
|
||||
lazy: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<BaseCard>
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ $t('settings.exchange_rate.title') }}
|
||||
</h6>
|
||||
<p class="mt-2 text-sm leading-snug text-gray-500" style="max-width: 680px">
|
||||
{{
|
||||
$t('settings.exchange_rate.description', {
|
||||
currency: companyStore.selectedCompanyCurrency.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
||||
<form action="" @submit.prevent="submitBulkUpdate">
|
||||
<ValidateEach
|
||||
v-for="(c, i) in exchangeRateStore.bulkCurrencies"
|
||||
:key="i"
|
||||
:state="c"
|
||||
:rules="currencyArrayRules"
|
||||
>
|
||||
<template #default="{ v }">
|
||||
<BaseInputGroup
|
||||
class="my-5"
|
||||
:label="`${c.code} to ${companyStore.selectedCompanyCurrency.code}`"
|
||||
:error="
|
||||
v.exchange_rate.$error && v.exchange_rate.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="c.exchange_rate"
|
||||
:addon="`1 ${c.code} =`"
|
||||
:invalid="v.exchange_rate.$error"
|
||||
@input="v.exchange_rate.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<span class="text-gray-500 sm:text-sm">
|
||||
{{ companyStore.selectedCompanyCurrency.code }}
|
||||
</span>
|
||||
</template>
|
||||
</BaseInput>
|
||||
<span class="text-gray-400 text-xs mt-2 font-light">
|
||||
{{
|
||||
$t('settings.exchange_rate.exchange_help_text', {
|
||||
currency: c.code,
|
||||
baseCurrency: companyStore.selectedCompanyCurrency.code,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
</ValidateEach>
|
||||
<div
|
||||
slot="footer"
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
mt-4
|
||||
pt-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton :loading="isSaving" variant="primary" type="submit">
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { computed, ref } from '@vue/runtime-core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers, numeric, decimal } from '@vuelidate/validators'
|
||||
import { ValidateEach } from '@vuelidate/components'
|
||||
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
let isLoading = ref(false)
|
||||
|
||||
const currencyArrayRules = {
|
||||
exchange_rate: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
decimal: helpers.withMessage(t('validation.valid_exchange_rate'), decimal),
|
||||
},
|
||||
}
|
||||
const v = useVuelidate()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
async function submitBulkUpdate() {
|
||||
v.value.$touch()
|
||||
if (v.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
isSaving.value = true
|
||||
let data = exchangeRateStore.bulkCurrencies.map((_c) => {
|
||||
return {
|
||||
id: _c.id,
|
||||
exchange_rate: _c.exchange_rate,
|
||||
}
|
||||
})
|
||||
let res = await exchangeRateStore.updateBulkExchangeRate({ currencies: data })
|
||||
if (res.data.success) {
|
||||
emit('update', res.data.success)
|
||||
}
|
||||
isSaving.value = false
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp] && store[storeProp].customFields.length > 0 && !isLoading
|
||||
"
|
||||
>
|
||||
<BaseInputGrid :layout="gridLayout">
|
||||
<SingleField
|
||||
v-for="(field, index) in store[storeProp].customFields"
|
||||
:key="field.id"
|
||||
:custom-field-scope="customFieldScope"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
:index="index"
|
||||
:field="field"
|
||||
/>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import lodash from 'lodash'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { watch } from 'vue'
|
||||
import SingleField from './CreateCustomFieldsSingle.vue'
|
||||
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
gridLayout: {
|
||||
type: String,
|
||||
default: 'two-column',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
customFieldScope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
getInitialCustomFields()
|
||||
|
||||
function mergeExistingValues() {
|
||||
if (props.isEdit) {
|
||||
props.store[props.storeProp].fields.forEach((field) => {
|
||||
const existingIndex = props.store[props.storeProp].customFields.findIndex(
|
||||
(f) => f.id === field.custom_field_id
|
||||
)
|
||||
|
||||
if (existingIndex > -1) {
|
||||
let value = field.default_answer
|
||||
|
||||
if (value && field.custom_field.type === 'DateTime') {
|
||||
value = moment(field.default_answer, 'YYYY-MM-DD HH:mm:ss').format(
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
}
|
||||
|
||||
props.store[props.storeProp].customFields[existingIndex] = {
|
||||
...field,
|
||||
id: field.custom_field_id,
|
||||
value: value,
|
||||
label: field.custom_field.label,
|
||||
options: field.custom_field.options,
|
||||
is_required: field.custom_field.is_required,
|
||||
placeholder: field.custom_field.placeholder,
|
||||
order: field.custom_field.order,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function getInitialCustomFields() {
|
||||
const res = await customFieldStore.fetchCustomFields({
|
||||
type: props.type,
|
||||
limit: 'all',
|
||||
})
|
||||
|
||||
let data = res.data.data
|
||||
|
||||
data.map((d) => (d.value = d.default_answer))
|
||||
|
||||
props.store[props.storeProp].customFields = lodash.sortBy(
|
||||
data,
|
||||
(_cf) => _cf.order
|
||||
)
|
||||
|
||||
mergeExistingValues()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].fields,
|
||||
(val) => {
|
||||
mergeExistingValues()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<BaseInputGroup
|
||||
:label="field.label"
|
||||
:required="field.is_required ? true : false"
|
||||
:error="v$.value.$error && v$.value.$errors[0].$message"
|
||||
>
|
||||
<component
|
||||
:is="getTypeComponent"
|
||||
v-model="field.value"
|
||||
:options="field.options"
|
||||
:invalid="v$.value.$error"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineAsyncComponent, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { helpers, requiredIf } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
customFieldScope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules = {
|
||||
value: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(props.field.is_required)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => props.field),
|
||||
{ $scope: props.customFieldScope }
|
||||
)
|
||||
|
||||
const getTypeComponent = computed(() => {
|
||||
if (props.field.type) {
|
||||
return defineAsyncComponent(() =>
|
||||
import(`./types/${props.field.type}Type.vue`)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseDatePicker v-model="date" enable-time />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: moment().format('YYYY-MM-DD hh:MM'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseDatePicker v-model="date" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import moment from 'moment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Date],
|
||||
default: moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<BaseMultiselect
|
||||
v-model="inputValue"
|
||||
:options="options"
|
||||
:label="label"
|
||||
:value-prop="valueProp"
|
||||
:object="object"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Object, Number],
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
valueProp: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
object: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="text" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="number" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="tel" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<BaseSwitch v-model="inputValue" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue === 1,
|
||||
set: (value) => {
|
||||
const intVal = value ? 1 : 0
|
||||
|
||||
emit('update:modelValue', intVal)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<BaseTextarea v-model="inputValue" :rows="rows" :name="inputName" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
rows: {
|
||||
type: String,
|
||||
default: '2',
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
default: 'description',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseTimePicker v-model="date" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import moment from 'moment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Date, Object],
|
||||
default: moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="url" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit customField -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_CUSTOM_FIELDS)"
|
||||
@click="editCustomField(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete customField -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_CUSTOM_FIELDS)"
|
||||
@click="removeCustomField(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editCustomField(id) {
|
||||
await customFieldStore.fetchCustomField(id)
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.custom_fields.edit_custom_field'),
|
||||
componentName: 'CustomFieldModal',
|
||||
size: 'sm',
|
||||
data: id,
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
async function removeCustomField(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.custom_fields.custom_field_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await customFieldStore.deleteCustomFields(id)
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="customerStore.isFetchingViewData">
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'customers.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Customer -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_CUSTOMER)"
|
||||
:to="`/admin/customers/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- View Customer -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'customers.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_CUSTOMER)
|
||||
"
|
||||
:to="`customers/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Customer -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_CUSTOMER)"
|
||||
@click="removeCustomer(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const customerStore = useCustomerStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
function removeCustomer(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('customers.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
customerStore.deleteCustomer({ ids: [id] }).then((response) => {
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'estimates.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else class="text-gray-500" name="DotsHorizontalIcon" />
|
||||
</template>
|
||||
|
||||
<!-- Copy PDF url -->
|
||||
<BaseDropdownItem
|
||||
v-if="route.name === 'estimates.view'"
|
||||
@click="copyPdfUrl"
|
||||
>
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Edit Estimate -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_ESTIMATE)"
|
||||
:to="`/admin/estimates/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_ESTIMATE)"
|
||||
@click="removeEstimate(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- View Estimate -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_ESTIMATE)
|
||||
"
|
||||
:to="`estimates/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Convert into Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)"
|
||||
@click="convertInToinvoice(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentTextIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as sent -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsSent(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Send Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
"
|
||||
@click="sendEstimate(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Resend Estimate -->
|
||||
<BaseDropdownItem v-if="canResendEstimate(row)" @click="sendEstimate(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.resend_estimate') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as Accepted -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'ACCEPTED' &&
|
||||
userStore.hasAbilities(abilities.EDIT_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsAccepted(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_accepted') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as Rejected -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'REJECTED' &&
|
||||
userStore.hasAbilities(abilities.EDIT_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsRejected(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="XCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_rejected') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const estimateStore = useEstimateStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
async function removeEstimate(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
id = id
|
||||
if (res) {
|
||||
estimateStore.deleteEstimate({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.table && props.table.refresh()
|
||||
|
||||
if (res.data) {
|
||||
router.push('/admin/estimates')
|
||||
}
|
||||
estimateStore.$patch((state) => {
|
||||
state.selectedEstimates = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function convertInToinvoice(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_conversion'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
estimateStore.convertToInvoice(id).then((res) => {
|
||||
if (res.data) {
|
||||
router.push(`/admin/invoices/${res.data.data.id}/edit`)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsSent(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_sent'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsSent(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function canResendEstimate(row) {
|
||||
return (
|
||||
(row.status == 'SENT' || row.status == 'VIEWED') &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
)
|
||||
}
|
||||
|
||||
async function sendEstimate(estimate) {
|
||||
modalStore.openModal({
|
||||
title: t('estimates.send_estimate'),
|
||||
componentName: 'SendEstimateModal',
|
||||
id: estimate.id,
|
||||
data: estimate,
|
||||
variant: 'lg',
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsAccepted(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_accepted'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'ACCEPTED',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsAccepted(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsRejected(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_rejected'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'REJECTED',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsRejected(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${props.row.unique_hash}`
|
||||
|
||||
let response = utils.copyTextToClipboard(pdfUrl)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
v-if="route.name === 'expenseCategorys.view'"
|
||||
variant="primary"
|
||||
>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit expenseCategory -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_EXPENSE)"
|
||||
@click="editExpenseCategory(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete expenseCategory -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||
@click="removeExpenseCategory(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCategoryStore } from '@/scripts/admin/stores/category'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const expenseCategoryStore = useCategoryStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editExpenseCategory(data) {
|
||||
expenseCategoryStore.fetchCategory(data)
|
||||
modalStore.openModal({
|
||||
title: t('settings.expense_category.edit_category'),
|
||||
componentName: 'CategoryModal',
|
||||
refreshData: props.loadData,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removeExpenseCategory(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.expense_category.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async () => {
|
||||
let response = await expenseCategoryStore.deleteCategory(id)
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'expenses.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit expense -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_EXPENSE)"
|
||||
:to="`/admin/expenses/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete expense -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||
@click="removeExpense(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const expenseStore = useExpenseStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeExpense(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('expenses.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
expenseStore.deleteExpense({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
261
resources/scripts/admin/components/dropdowns/InvoiceIndexDropdown.vue
Executable file
261
resources/scripts/admin/components/dropdowns/InvoiceIndexDropdown.vue
Executable file
@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'invoices.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Invoice -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_INVOICE)"
|
||||
:to="`/admin/invoices/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem v-show="row.allow_edit">
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Copy PDF url -->
|
||||
<BaseDropdownItem v-if="route.name === 'invoices.view'" @click="copyPdfUrl">
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- View Invoice -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'invoices.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_INVOICE)
|
||||
"
|
||||
:to="`/admin/invoices/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Send Invoice Mail -->
|
||||
<BaseDropdownItem v-if="canSendInvoice(row)" @click="sendInvoice(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Resend Invoice -->
|
||||
<BaseDropdownItem v-if="canReSendInvoice(row)" @click="sendInvoice(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.resend_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Record payment -->
|
||||
<router-link :to="`/admin/payments/${row.id}/create`">
|
||||
<BaseDropdownItem
|
||||
v-if="row.status == 'SENT' && route.name !== 'invoices.view'"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CreditCardIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.record_payment') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Mark as sent Invoice -->
|
||||
<BaseDropdownItem v-if="canSendInvoice(row)" @click="onMarkAsSent(row.id)">
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Clone Invoice into new invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)"
|
||||
@click="cloneInvoiceData(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentTextIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.clone_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Delete Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_INVOICE)"
|
||||
@click="removeInvoice(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
function canReSendInvoice(row) {
|
||||
return (
|
||||
(row.status == 'SENT' || row.status == 'VIEWED') &&
|
||||
userStore.hasAbilities(abilities.SEND_INVOICE)
|
||||
)
|
||||
}
|
||||
|
||||
function canSendInvoice(row) {
|
||||
return (
|
||||
row.status == 'DRAFT' &&
|
||||
route.name !== 'invoices.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_INVOICE)
|
||||
)
|
||||
}
|
||||
|
||||
async function removeInvoice(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
id = id
|
||||
if (res) {
|
||||
invoiceStore.deleteInvoice({ ids: [id] }).then((res) => {
|
||||
if (res.data.success) {
|
||||
router.push('/admin/invoices')
|
||||
props.table && props.table.refresh()
|
||||
|
||||
invoiceStore.$patch((state) => {
|
||||
state.selectedInvoices = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function cloneInvoiceData(data) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_clone'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
invoiceStore.cloneInvoice(data).then((res) => {
|
||||
router.push(`/admin/invoices/${res.data.data.id}/edit`)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsSent(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.invoice_mark_as_sent'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
if (response) {
|
||||
invoiceStore.markAsSent(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function sendInvoice(invoice) {
|
||||
modalStore.openModal({
|
||||
title: t('invoices.send_invoice'),
|
||||
componentName: 'SendInvoiceModal',
|
||||
id: invoice.id,
|
||||
data: invoice,
|
||||
variant: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/invoices/pdf/${props.row.unique_hash}`
|
||||
|
||||
utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'items.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit item -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_ITEM)"
|
||||
:to="`/admin/items/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete item -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_ITEM)"
|
||||
@click="removeItem(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const itemStore = useItemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeItem(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('items.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
itemStore.deleteItem({ ids: [id] }).then((response) => {
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'notes.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit note -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
@click="editNote(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete note -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
@click="removeNote(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const noteStore = useNotesStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editNote(data) {
|
||||
noteStore.fetchNote(data)
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.notes.edit_note'),
|
||||
componentName: 'NoteModal',
|
||||
size: 'md',
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
function removeNote(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.customization.notes.note_confirm_delete'),
|
||||
yesLabel: t('general.yes'),
|
||||
noLabel: t('general.no'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async () => {
|
||||
let response = await noteStore.deleteNote(id)
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.deleted_message'),
|
||||
})
|
||||
} else {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('settings.customization.notes.already_in_use'),
|
||||
})
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="contentLoading">
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'payments.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Copy pdf url -->
|
||||
<BaseDropdown-item
|
||||
v-if="
|
||||
route.name === 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_PAYMENT)
|
||||
"
|
||||
class="rounded-md"
|
||||
@click="copyPdfUrl"
|
||||
>
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdown-item>
|
||||
|
||||
<!-- edit payment -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_PAYMENT)"
|
||||
:to="`/admin/payments/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- view payment -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_PAYMENT)
|
||||
"
|
||||
:to="`/admin/payments/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Send Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_PAYMENT)
|
||||
"
|
||||
@click="sendPayment(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('payments.send_payment') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete payment -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_PAYMENT)"
|
||||
@click="removePayment(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
contentLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const paymentStore = usePaymentStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removePayment(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('payments.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await paymentStore.deletePayment({ ids: [id] })
|
||||
router.push(`/admin/payments`)
|
||||
props.table && props.table.refresh()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/payments/pdf/${props.row?.unique_hash}`
|
||||
|
||||
$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
|
||||
async function sendPayment(payment) {
|
||||
modalStore.openModal({
|
||||
title: t('payments.send_payment'),
|
||||
componentName: 'SendPaymentModal',
|
||||
id: payment.id,
|
||||
data: payment,
|
||||
variant: 'lg',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'paymentModes.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit paymentMode -->
|
||||
<BaseDropdownItem @click="editPaymentMode(row.id)">
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete paymentMode -->
|
||||
<BaseDropdownItem @click="removePaymentMode(row.id)">
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const paymentStore = usePaymentStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editPaymentMode(id) {
|
||||
paymentStore.fetchPaymentMode(id)
|
||||
modalStore.openModal({
|
||||
title: t('settings.payment_modes.edit_payment_mode'),
|
||||
componentName: 'PaymentModeModal',
|
||||
refreshData: props.loadData && props.loadData,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removePaymentMode(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.payment_modes.payment_mode_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await paymentStore.deletePaymentMode(id)
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="recurringInvoiceStore.isFetchingViewData">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
v-if="route.name === 'recurring-invoices.view'"
|
||||
variant="primary"
|
||||
>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Recurring Invoice -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_RECURRING_INVOICE)"
|
||||
:to="`/admin/recurring-invoices/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- View Recurring Invoice -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'recurring-invoices.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_RECURRING_INVOICE)
|
||||
"
|
||||
:to="`recurring-invoices/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Recurring Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_RECURRING_INVOICE)"
|
||||
@click="removeMultipleRecurringInvoices(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
async function removeMultipleRecurringInvoices(id = null) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await recurringInvoiceStore
|
||||
.deleteMultipleRecurringInvoices(id)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
props.table && props.table.refresh()
|
||||
recurringInvoiceStore.$patch((state) => {
|
||||
state.selectedRecurringInvoices = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('recurring_invoices.deleted_message', 2),
|
||||
})
|
||||
} else if (res.data.error) {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: res.data.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'roles.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit role -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.currentUser.is_owner"
|
||||
@click="editRole(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete role -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.currentUser.is_owner"
|
||||
@click="removeRole(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoleStore } from '@/scripts/admin/stores/role'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const roleStore = useRoleStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editRole(id) {
|
||||
Promise.all([
|
||||
await roleStore.fetchAbilities(),
|
||||
await roleStore.fetchRole(id),
|
||||
]).then(() => {
|
||||
modalStore.openModal({
|
||||
title: t('settings.roles.edit_role'),
|
||||
componentName: 'RolesModal',
|
||||
size: 'lg',
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function removeRole(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.roles.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await roleStore.deleteRole(id).then((response) => {
|
||||
if (response.data) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'tax-types.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit tax-type -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_TAX_TYPE)"
|
||||
@click="editTaxType(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete tax-type -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_TAX_TYPE)"
|
||||
@click="removeTaxType(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editTaxType(id) {
|
||||
await taxTypeStore.fetchTaxType(id)
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.edit_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: props.loadData && props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
function removeTaxType(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.tax_types.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
let response = await taxTypeStore.deleteTaxType(id)
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'users.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit user -->
|
||||
<router-link :to="`/admin/users/${row.id}/edit`">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete user -->
|
||||
<BaseDropdownItem @click="removeUser(row.id)">
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUsersStore } from '@/scripts/admin/stores/users'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const usersStore = useUsersStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeUser(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('users.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
usersStore.deleteUser({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
|
||||
<td colspan="5" class="p-0 text-left align-top">
|
||||
<table class="w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-5
|
||||
h-5
|
||||
mt-2
|
||||
text-gray-300
|
||||
cursor-move
|
||||
handle
|
||||
mr-2
|
||||
"
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
<BaseItemSelect
|
||||
type="Invoice"
|
||||
:item="itemData"
|
||||
:invalid="v$.name.$error"
|
||||
:invalid-description="v$.description.$error"
|
||||
:taxes="itemData.taxes"
|
||||
:index="index"
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
@search="searchVal"
|
||||
@select="onSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<BaseInput
|
||||
v-model="quantity"
|
||||
:invalid="v$.quantity.$error"
|
||||
:content-loading="loading"
|
||||
type="number"
|
||||
small
|
||||
min="0"
|
||||
step="any"
|
||||
@change="syncItemToStore()"
|
||||
@input="v$.quantity.$touch()"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-auto flex-fill bd-highlight">
|
||||
<div class="relative w-full">
|
||||
<BaseMoney
|
||||
:key="selectedCurrency"
|
||||
v-model="price"
|
||||
:invalid="v$.price.$error"
|
||||
:content-loading="loading"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex" style="width: 120px" role="group">
|
||||
<BaseInput
|
||||
v-model="discount"
|
||||
:invalid="v$.discount_val.$error"
|
||||
:content-loading="loading"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
:content-loading="loading"
|
||||
class="rounded-tr-md rounded-br-md !p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
itemData.discount_type == 'fixed'
|
||||
? currency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<BaseFormatMoney
|
||||
v-else
|
||||
:amount="total"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2">
|
||||
<BaseIcon
|
||||
v-if="showRemoveButton"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
name="TrashIcon"
|
||||
@click="store.removeItem(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="store[storeProp].tax_per_item === 'YES'">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<ItemTax
|
||||
v-for="(tax, index1) in itemData.taxes"
|
||||
v-else
|
||||
:key="tax.id"
|
||||
:index="index1"
|
||||
:item-index="index"
|
||||
:tax-data="tax"
|
||||
:taxes="itemData.taxes"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total="subtotal"
|
||||
:currency="currency"
|
||||
:update-items="syncItemToStore"
|
||||
:ability="abilities.CREATE_INVOICE"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/admin/stub/tax'
|
||||
import ItemTax from './CreateItemRowTax.vue'
|
||||
import { sumBy } from 'lodash'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
import {
|
||||
required,
|
||||
between,
|
||||
maxLength,
|
||||
helpers,
|
||||
minValue,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
invoiceItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove', 'itemValidate'])
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const itemStore = useItemStore()
|
||||
|
||||
let route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const quantity = computed({
|
||||
get: () => {
|
||||
return props.itemData.quantity
|
||||
},
|
||||
set: (newValue) => {
|
||||
updateItemAttribute('quantity', parseFloat(newValue))
|
||||
},
|
||||
})
|
||||
|
||||
const price = computed({
|
||||
get: () => {
|
||||
const price = props.itemData.price
|
||||
|
||||
if (parseFloat(price) > 0) {
|
||||
return price / 100
|
||||
}
|
||||
|
||||
return price
|
||||
},
|
||||
|
||||
set: (newValue) => {
|
||||
if (parseFloat(newValue) > 0) {
|
||||
let price = Math.round(newValue * 100)
|
||||
|
||||
updateItemAttribute('price', price)
|
||||
} else {
|
||||
updateItemAttribute('price', newValue)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
|
||||
|
||||
const discount = computed({
|
||||
get: () => {
|
||||
return props.itemData.discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
|
||||
} else {
|
||||
updateItemAttribute('discount_val', Math.round(newValue * 100))
|
||||
}
|
||||
|
||||
updateItemAttribute('discount', newValue)
|
||||
},
|
||||
})
|
||||
|
||||
const total = computed(() => {
|
||||
return subtotal.value - props.itemData.discount_val
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
const showRemoveButton = computed(() => {
|
||||
if (props.store[props.storeProp].items.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const totalSimpleTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalCompoundTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
quantity: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.qty_must_greater_than_zero'),
|
||||
minValue(0)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.amount_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
price: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.number_length_minvalue'),
|
||||
minValue(1)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.price_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
discount_val: {
|
||||
between: helpers.withMessage(
|
||||
t('validation.discount_maxlength'),
|
||||
between(
|
||||
0,
|
||||
computed(() => subtotal.value)
|
||||
)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.notes_maxlength'),
|
||||
maxLength(65000)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => props.store[props.storeProp].items[props.index]),
|
||||
{ $scope: props.itemValidationScope }
|
||||
)
|
||||
|
||||
//
|
||||
// if (
|
||||
// route.params.id &&
|
||||
// (props.store[props.storeProp].tax_per_item === 'YES' || 'NO')
|
||||
// ) {
|
||||
// if (props.store[props.storeProp].items[props.index].taxes === undefined) {
|
||||
// props.store.$patch((state) => {
|
||||
// state[props.storeProp].items[props.index].taxes = [
|
||||
// { ...TaxStub, id: Guid.raw() },
|
||||
// ]
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
function updateTax(data) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
|
||||
})
|
||||
|
||||
let lastTax = props.itemData.taxes[props.itemData.taxes.length - 1]
|
||||
|
||||
if (lastTax?.tax_type_id !== 0) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].taxes.push({
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function searchVal(val) {
|
||||
updateItemAttribute('name', val)
|
||||
}
|
||||
|
||||
function onSelectItem(itm) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].name = itm.name
|
||||
state[props.storeProp].items[props.index].price = itm.price
|
||||
state[props.storeProp].items[props.index].item_id = itm.id
|
||||
state[props.storeProp].items[props.index].description = itm.description
|
||||
|
||||
if (itm.unit) {
|
||||
state[props.storeProp].items[props.index].unit_name = itm.unit.name
|
||||
}
|
||||
|
||||
if (props.store[props.storeProp].tax_per_item === 'YES' && itm.taxes) {
|
||||
let index = 0
|
||||
|
||||
itm.taxes.forEach((tax) => {
|
||||
updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
|
||||
if (state[props.storeProp].exchange_rate) {
|
||||
state[props.storeProp].items[props.index].price /=
|
||||
state[props.storeProp].exchange_rate
|
||||
}
|
||||
})
|
||||
|
||||
itemStore.fetchItems()
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function selectFixed() {
|
||||
if (props.itemData.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute('discount_val', Math.round(props.itemData.discount * 100))
|
||||
updateItemAttribute('discount_type', 'fixed')
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute(
|
||||
'discount_val',
|
||||
(subtotal.value * props.itemData.discount) / 100
|
||||
)
|
||||
|
||||
updateItemAttribute('discount_type', 'percentage')
|
||||
}
|
||||
|
||||
function syncItemToStore() {
|
||||
let itemTaxes = props.store[props.storeProp]?.items[props.index]?.taxes
|
||||
|
||||
if (!itemTaxes) {
|
||||
itemTaxes = []
|
||||
}
|
||||
|
||||
let data = {
|
||||
...props.store[props.storeProp].items[props.index],
|
||||
index: props.index,
|
||||
total: total.value,
|
||||
sub_total: subtotal.value,
|
||||
totalSimpleTax: totalSimpleTax.value,
|
||||
totalCompoundTax: totalCompoundTax.value,
|
||||
totalTax: totalTax.value,
|
||||
tax: totalTax.value,
|
||||
taxes: [...itemTaxes],
|
||||
}
|
||||
|
||||
props.store.updateItem(data)
|
||||
}
|
||||
|
||||
function updateItemAttribute(attribute, value) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index][attribute] = value
|
||||
})
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center text-base" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('invoices.item.tax') }}
|
||||
</label>
|
||||
|
||||
<BaseMultiselect
|
||||
v-model="selectedTax"
|
||||
value-prop="id"
|
||||
:options="filteredTypes"
|
||||
:placeholder="$t('general.select_a_tax')"
|
||||
open-direction="top"
|
||||
track-by="name"
|
||||
searchable
|
||||
object
|
||||
label="name"
|
||||
@update:modelValue="(val) => onSelectTax(val)"
|
||||
>
|
||||
<template #singlelabel="{ value }">
|
||||
<div class="absolute left-3.5">
|
||||
{{ value.name }} - {{ value.percent }} %
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #option="{ option }">
|
||||
{{ option.name }} - {{ option.percent }} %
|
||||
</template>
|
||||
|
||||
<template v-if="userStore.hasAbilities(ability)" #action>
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
cursor-pointer
|
||||
py-2
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
|
||||
|
||||
<label
|
||||
class="ml-2 text-sm leading-none text-primary-400 cursor-pointer"
|
||||
>{{ $t('invoices.add_new_tax') }}</label
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
</BaseMultiselect>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-right" style="flex: 3">
|
||||
<BaseFormatMoney :amount="taxAmount" :currency="currency" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<BaseIcon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
name="TrashIcon"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
@click="removeTax(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject, reactive, watch } from 'vue'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
|
||||
const props = defineProps({
|
||||
ability: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
updateItems: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['remove', 'update'])
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const selectedTax = ref(null)
|
||||
const localTax = reactive({ ...props.taxData })
|
||||
const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredTypes = computed(() => {
|
||||
const clonedTypes = taxTypeStore.taxTypes.map((a) => ({ ...a }))
|
||||
|
||||
return clonedTypes.map((taxType) => {
|
||||
let found = props.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.disabled = true
|
||||
} else {
|
||||
taxType.disabled = false
|
||||
}
|
||||
|
||||
return taxType
|
||||
})
|
||||
})
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (localTax.compound_tax && props.total) {
|
||||
return ((props.total + props.totalTax) * localTax.percent) / 100
|
||||
}
|
||||
|
||||
if (props.total && localTax.percent) {
|
||||
return (props.total * localTax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.total,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.totalTax,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
// Set SelectedTax
|
||||
if (props.taxData.tax_type_id > 0) {
|
||||
selectedTax.value = taxTypeStore.taxTypes.find(
|
||||
(_type) => _type.id === props.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
updateRowTax()
|
||||
|
||||
function onSelectTax(val) {
|
||||
localTax.percent = val.percent
|
||||
localTax.tax_type_id = val.id
|
||||
localTax.compound_tax = val.compound_tax
|
||||
localTax.name = val.name
|
||||
|
||||
updateRowTax()
|
||||
}
|
||||
|
||||
function updateRowTax() {
|
||||
if (localTax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update', {
|
||||
index: props.index,
|
||||
item: {
|
||||
...localTax,
|
||||
amount: taxAmount.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openTaxModal() {
|
||||
let data = {
|
||||
itemIndex: props.itemIndex,
|
||||
taxIndex: props.index,
|
||||
}
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
data: data,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removeTax(index) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<table class="text-center item-table min-w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<thead class="bg-white border border-gray-200 border-solid">
|
||||
<tr>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pl-7">
|
||||
{{ $tc('items.item', 2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pr-10 column-heading">
|
||||
{{ $t('invoices.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="store[storeProp].items"
|
||||
item-key="id"
|
||||
tag="tbody"
|
||||
handle=".handle"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<Item
|
||||
:key="element.id"
|
||||
:index="index"
|
||||
:item-data="element"
|
||||
:loading="isLoading"
|
||||
:currency="defaultCurrency"
|
||||
:item-validation-scope="itemValidationScope"
|
||||
:invoice-items="store[storeProp].items"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-6
|
||||
py-3
|
||||
text-base
|
||||
border border-t-0 border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
text-primary-400
|
||||
hover:bg-primary-100
|
||||
"
|
||||
@click="store.addItem"
|
||||
>
|
||||
<BaseIcon name="PlusCircleIcon" class="mr-2" />
|
||||
{{ $t('general.add_new_item') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { computed } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import Item from './CreateItemRow.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String, null],
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="mb-6">
|
||||
<div
|
||||
class="z-20 text-sm font-semibold leading-5 text-primary-400 float-right"
|
||||
>
|
||||
<SelectNotePopup :type="type" @select="onSelectNote" />
|
||||
</div>
|
||||
<label class="text-gray-800 font-medium mb-4 text-sm">
|
||||
{{ $t('invoices.notes') }}
|
||||
</label>
|
||||
<BaseCustomInput
|
||||
v-model="store[storeProp].notes"
|
||||
:content-loading="store.isFetchingInitialSettings"
|
||||
:fields="fields"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import SelectNotePopup from '../SelectNotePopup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fields: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
function onSelectNote(data) {
|
||||
props.store[props.storeProp].notes = '' + data.notes
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
px-5
|
||||
py-4
|
||||
mt-6
|
||||
bg-white
|
||||
border border-gray-200 border-solid
|
||||
rounded
|
||||
md:min-w-[390px]
|
||||
min-w-[300px]
|
||||
lg:mt-7
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.sub_total') }}
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney
|
||||
:amount="store.getSubTotal"
|
||||
:currency="defaultCurrency"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tax in itemWiseTaxes"
|
||||
:key="tax.tax_type_id"
|
||||
class="flex items-center justify-between w-full"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-500 uppercase"
|
||||
>
|
||||
{{ tax.name }} - {{ tax.percent }}%
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].discount_per_item === 'NO' ||
|
||||
store[storeProp].discount_per_item === null
|
||||
"
|
||||
class="flex items-center justify-between w-full mt-2"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.discount') }}
|
||||
</label>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex" style="width: 140px" role="group">
|
||||
<BaseInput
|
||||
v-model="totalDiscount"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
class="rounded-tr-md rounded-br-md p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
store[storeProp].discount_type == 'fixed'
|
||||
? defaultCurrency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
>
|
||||
<Tax
|
||||
v-for="(tax, index) in taxes"
|
||||
:key="tax.id"
|
||||
:index="index"
|
||||
:tax="tax"
|
||||
:taxes="taxes"
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
@remove="removeTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
ref="taxModal"
|
||||
class="float-right pt-2 pb-4"
|
||||
>
|
||||
<SelectTaxPopup
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
:type="taxPopupType"
|
||||
@select:taxType="onSelectTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
w-full
|
||||
pt-2
|
||||
mt-5
|
||||
border-t border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label
|
||||
>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
text-lg
|
||||
uppercase
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="store.getTotal" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import Guid from 'guid'
|
||||
import Tax from './CreateTotalTaxes.vue'
|
||||
import TaxStub from '@/scripts/admin/stub/abilities'
|
||||
import SelectTaxPopup from './SelectTaxPopup.vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const taxModal = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
taxPopupType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const totalDiscount = computed({
|
||||
get: () => {
|
||||
return props.store[props.storeProp].discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
(props.store.getSubTotal * newValue) / 100
|
||||
)
|
||||
} else {
|
||||
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
|
||||
}
|
||||
props.store[props.storeProp].discount = newValue
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () => props.store[props.storeProp].taxes,
|
||||
set: (value) => {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const itemWiseTaxes = computed(() => {
|
||||
let taxes = []
|
||||
props.store[props.storeProp].items.forEach((item) => {
|
||||
if (item.taxes) {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return taxes
|
||||
})
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
function selectFixed() {
|
||||
if (props.store[props.storeProp].discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
props.store[props.storeProp].discount * 100
|
||||
)
|
||||
props.store[props.storeProp].discount_type = 'fixed'
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val =
|
||||
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
|
||||
props.store[props.storeProp].discount_type = 'percentage'
|
||||
}
|
||||
|
||||
function onSelectTax(selectedTax) {
|
||||
let amount = 0
|
||||
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
selectedTax.percent) /
|
||||
100
|
||||
)
|
||||
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
|
||||
amount = Math.round(
|
||||
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
|
||||
)
|
||||
}
|
||||
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount,
|
||||
}
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function updateTax(data) {
|
||||
const tax = props.store[props.storeProp].taxes.find(
|
||||
(tax) => tax.id === data.id
|
||||
)
|
||||
if (tax) {
|
||||
Object.assign(tax, { ...data })
|
||||
}
|
||||
}
|
||||
|
||||
function removeTax(id) {
|
||||
const index = props.store[props.storeProp].taxes.findIndex(
|
||||
(tax) => tax.id === id
|
||||
)
|
||||
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full mt-2 text-sm">
|
||||
<label class="font-semibold leading-5 text-gray-500 uppercase">
|
||||
{{ tax.name }} ({{ tax.percent }} %)
|
||||
</label>
|
||||
<label class="flex items-center justify-center text-lg text-black">
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="currency" />
|
||||
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="h-5 ml-2 cursor-pointer"
|
||||
@click="$emit('remove', tax.id)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch, inject, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (props.tax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
return Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
props.tax.percent) /
|
||||
100
|
||||
)
|
||||
}
|
||||
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
|
||||
return Math.round(
|
||||
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
|
||||
)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.store.getSubtotalWithDiscount) {
|
||||
updateTax()
|
||||
}
|
||||
if (props.store.getTotalSimpleTax) {
|
||||
updateTax()
|
||||
}
|
||||
})
|
||||
|
||||
function updateTax() {
|
||||
emit('update', {
|
||||
...props.tax,
|
||||
amount: taxAmount.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<BaseInputGroup
|
||||
v-if="store.showExchangeRate && selectedCurrency"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:label="$t('settings.exchange_rate.exchange_rate')"
|
||||
:error="v.exchange_rate.$error && v.exchange_rate.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<template #labelRight>
|
||||
<div v-if="hasActiveProvider && isEdit">
|
||||
<BaseIcon
|
||||
v-tooltip="{ content: 'Fetch Latest Exchange rate' }"
|
||||
name="RefreshIcon"
|
||||
:class="`h-4 w-4 text-primary-500 cursor-pointer outline-none ${
|
||||
isFetching
|
||||
? ' animate-spin rotate-180 cursor-not-allowed pointer-events-none '
|
||||
: ''
|
||||
}`"
|
||||
@click="getCurrenctExchangeRate(customerCurrency)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<BaseInput
|
||||
v-model="store[storeProp].exchange_rate"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:addon="`1 ${selectedCurrency.code} =`"
|
||||
:disabled="isFetching"
|
||||
@input="v.exchange_rate.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<span class="text-gray-500 sm:text-sm">
|
||||
{{ companyCurrency.code }}
|
||||
</span>
|
||||
</template>
|
||||
</BaseInput>
|
||||
<span class="text-gray-400 text-xs mt-2 font-light">
|
||||
{{
|
||||
$t('settings.exchange_rate.exchange_help_text', {
|
||||
currency: selectedCurrency.code,
|
||||
baseCurrency: companyCurrency.code,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
|
||||
const props = defineProps({
|
||||
v: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customerCurrency: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const globalStore = useGlobalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const hasActiveProvider = ref(false)
|
||||
let isFetching = ref(false)
|
||||
|
||||
globalStore.fetchCurrencies()
|
||||
|
||||
const companyCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
return globalStore.currencies.find(
|
||||
(c) => c.id === props.store[props.storeProp].currency_id
|
||||
)
|
||||
})
|
||||
|
||||
const isCurrencyDiffrent = computed(() => {
|
||||
return companyCurrency.value.id !== props.customerCurrency
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].customer,
|
||||
(v) => {
|
||||
setCustomerCurrency(v)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].currency_id,
|
||||
(v) => {
|
||||
onChangeCurrency(v)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => props.customerCurrency,
|
||||
(v) => {
|
||||
if (v && props.isEdit) {
|
||||
checkForActiveProvider(v)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function checkForActiveProvider() {
|
||||
if (isCurrencyDiffrent.value) {
|
||||
exchangeRateStore
|
||||
.checkForActiveProvider(props.customerCurrency)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
hasActiveProvider.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomerCurrency(v) {
|
||||
if (v) {
|
||||
props.store[props.storeProp].currency_id = v.currency.id
|
||||
} else {
|
||||
props.store[props.storeProp].currency_id = companyCurrency.value.id
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangeCurrency(v) {
|
||||
if (v !== companyCurrency.value.id) {
|
||||
if (!props.isEdit && v) {
|
||||
await getCurrenctExchangeRate(v)
|
||||
}
|
||||
|
||||
props.store.showExchangeRate = true
|
||||
} else {
|
||||
props.store.showExchangeRate = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrenctExchangeRate(v) {
|
||||
isFetching.value = true
|
||||
exchangeRateStore
|
||||
.getCurrentExchangeRate(v)
|
||||
.then((res) => {
|
||||
if (res.data && !res.data.error) {
|
||||
props.store[props.storeProp].exchange_rate = res.data.exchangeRate[0]
|
||||
} else {
|
||||
props.store[props.storeProp].exchange_rate = ''
|
||||
}
|
||||
isFetching.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.store.showExchangeRate = false
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<TaxationAddressModal @addTax="addSalesTax" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {} from '@/scripts/admin/stores/recurring-invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, watch, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import TaxationAddressModal from '@/scripts/admin/components/modal-components/TaxationAddressModal.vue'
|
||||
const SALES_TAX_US = 'Sales Tax'
|
||||
const SALES_TAX_MODULE = 'MODULE'
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const { t } = useI18n()
|
||||
import { isEqual, pick } from 'lodash'
|
||||
|
||||
const fetchingTax = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
customer: {
|
||||
type: [Object],
|
||||
default: null,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const isSalesTaxTypeBilling = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_address_type === 'billing'
|
||||
: companyStore.selectedCompanySettings.sales_tax_address_type === 'billing'
|
||||
})
|
||||
|
||||
const salesTaxEnabled = computed(() => {
|
||||
return companyStore.selectedCompanySettings.sales_tax_us_enabled === 'YES'
|
||||
})
|
||||
|
||||
const salesTaxCustomerLevel = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_type === 'customer_level'
|
||||
: companyStore.selectedCompanySettings.sales_tax_type === 'customer_level'
|
||||
})
|
||||
|
||||
const salesTaxCompanyLevel = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_type === 'company_level'
|
||||
: companyStore.selectedCompanySettings.sales_tax_type === 'company_level'
|
||||
})
|
||||
|
||||
const addressData = computed(() => {
|
||||
if (salesTaxCustomerLevel.value && isAddressAvailable.value) {
|
||||
let address = isSalesTaxTypeBilling.value
|
||||
? props.customer.billing
|
||||
: props.customer.shipping
|
||||
return {
|
||||
address: pick(address, ['address_street_1', 'city', 'state', 'zip']),
|
||||
customer_id: props.customer.id,
|
||||
}
|
||||
} else if (salesTaxCompanyLevel.value && isAddressAvailable.value) {
|
||||
return {
|
||||
address: pick(address, ['address_street_1', 'city', 'state', 'zip']),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isAddressAvailable = computed(() => {
|
||||
if (salesTaxCustomerLevel.value) {
|
||||
let address = isSalesTaxTypeBilling.value
|
||||
? props.customer?.billing
|
||||
: props.customer?.shipping
|
||||
|
||||
return hasAddress(address)
|
||||
} else if (salesTaxCompanyLevel.value) {
|
||||
return hasAddress(companyStore.selectedCompany.address)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.customer,
|
||||
(v, o) => {
|
||||
if (v && o && salesTaxCustomerLevel.value) {
|
||||
// call if customer changed address
|
||||
isCustomerAddressChanged(v, o)
|
||||
return
|
||||
}
|
||||
if (!isAddressAvailable.value && salesTaxCustomerLevel.value && v) {
|
||||
setTimeout(() => {
|
||||
openAddressModal()
|
||||
}, 500)
|
||||
} else if (salesTaxCustomerLevel.value && v) {
|
||||
fetchSalesTax()
|
||||
} else if (salesTaxCustomerLevel.value && !v) {
|
||||
removeSalesTax()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Open modal for company address
|
||||
onMounted(() => {
|
||||
if (salesTaxCompanyLevel.value) {
|
||||
isAddressAvailable.value ? fetchSalesTax() : openAddressModal()
|
||||
}
|
||||
})
|
||||
|
||||
function hasAddress(address) {
|
||||
if (!address) return false
|
||||
|
||||
return (
|
||||
address.address_street_1 && address.city && address.state && address.zip
|
||||
)
|
||||
}
|
||||
|
||||
function isCustomerAddressChanged(newV, oldV) {
|
||||
const newData = isSalesTaxTypeBilling.value ? newV.billing : newV.shipping
|
||||
const oldData = isSalesTaxTypeBilling.value ? oldV.billing : oldV.shipping
|
||||
|
||||
const newAdd = pick(newData, ['address_street_1', 'city', 'state', 'zip'])
|
||||
const oldAdd = pick(oldData, ['address_street_1', 'city', 'state', 'zip'])
|
||||
!isEqual(newAdd, oldAdd) ? fetchSalesTax() : ''
|
||||
}
|
||||
|
||||
function openAddressModal() {
|
||||
if (!salesTaxEnabled.value) return
|
||||
let modalData = null
|
||||
let title = ''
|
||||
if (salesTaxCustomerLevel.value) {
|
||||
if (isSalesTaxTypeBilling.value) {
|
||||
modalData = props.customer?.billing
|
||||
title = t('settings.taxations.add_billing_address')
|
||||
} else {
|
||||
modalData = props.customer?.shipping
|
||||
title = t('settings.taxations.add_shipping_address')
|
||||
}
|
||||
} else {
|
||||
modalData = companyStore.selectedCompany.address
|
||||
title = t('settings.taxations.add_company_address')
|
||||
}
|
||||
|
||||
modalStore.openModal({
|
||||
title: title,
|
||||
content: t('settings.taxations.modal_description'),
|
||||
componentName: 'TaxationAddressModal',
|
||||
data: modalData,
|
||||
id: salesTaxCustomerLevel.value ? props.customer.id : '',
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchSalesTax() {
|
||||
if (!salesTaxEnabled.value) return
|
||||
|
||||
fetchingTax.value = true
|
||||
await taxTypeStore
|
||||
.fetchSalesTax(addressData.value)
|
||||
.then((res) => {
|
||||
addSalesTax(res.data.data)
|
||||
fetchingTax.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.data.error) {
|
||||
setTimeout(() => {
|
||||
openAddressModal()
|
||||
}, 500)
|
||||
}
|
||||
fetchingTax.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function addSalesTax(tax) {
|
||||
tax.tax_type_id = tax.id
|
||||
|
||||
const i = props.store[props.storeProp].taxes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
|
||||
if (i > -1) {
|
||||
Object.assign(props.store[props.storeProp].taxes[i], tax)
|
||||
} else {
|
||||
props.store[props.storeProp].taxes.push(tax)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSalesTax() {
|
||||
// remove from total taxes
|
||||
const i = props.store[props.storeProp].taxes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
i > -1 ? props.store[props.storeProp].taxes.splice(i, 1) : ''
|
||||
|
||||
// remove from tax-type list
|
||||
let pos = taxTypeStore.taxTypes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
|
||||
pos > -1 ? taxTypeStore.taxTypes.splice(pos, 1) : ''
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="w-full mt-4 tax-select">
|
||||
<Popover v-slot="{ isOpen }" class="relative">
|
||||
<PopoverButton
|
||||
:class="isOpen ? '' : 'text-opacity-90'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
text-sm
|
||||
font-medium
|
||||
text-primary-400
|
||||
focus:outline-none focus:border-none
|
||||
"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PlusIcon"
|
||||
class="w-4 h-4 font-medium text-primary-400"
|
||||
/>
|
||||
{{ $t('settings.tax_types.add_tax') }}
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Tax Select Popup -->
|
||||
<div class="relative w-full max-w-md px-4">
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
style="min-width: 350px; margin-left: 62px; top: -28px"
|
||||
class="absolute z-10 px-4 py-2 -translate-x-full sm:px-0"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
shadow-lg
|
||||
ring-1 ring-black ring-opacity-5
|
||||
"
|
||||
>
|
||||
<!-- Tax Search Input -->
|
||||
|
||||
<div class="relative bg-white">
|
||||
<div class="relative p-4">
|
||||
<BaseInput
|
||||
v-model="textSearch"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
class="text-black"
|
||||
>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<!-- List of Taxes -->
|
||||
<div
|
||||
v-if="filteredTaxType.length > 0"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
overflow-auto
|
||||
list
|
||||
max-h-36
|
||||
border-t border-gray-200
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(taxType, index) in filteredTaxType"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-gray-100 cursor-not-allowed opacity-50 pointer-events-none':
|
||||
taxes.find((val) => {
|
||||
return val.tax_type_id === taxType.id
|
||||
}),
|
||||
}"
|
||||
tabindex="2"
|
||||
class="
|
||||
px-6
|
||||
py-4
|
||||
border-b border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:bg-gray-100 hover:cursor-pointer
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectTaxType(taxType, close)"
|
||||
>
|
||||
<div class="flex justify-between px-2">
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
leading-tight
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.name }}
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.percent }} %
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex justify-center p-5 text-gray-400">
|
||||
<label class="text-base text-gray-500 cursor-pointer">
|
||||
{{ $t('general.no_tax_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add new Tax action -->
|
||||
<button
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_TAX_TYPE)"
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
h-10
|
||||
px-2
|
||||
py-3
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxTypeModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('estimates.add_new_tax') }}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref, inject, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select:taxType'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const textSearch = ref(null)
|
||||
|
||||
const filteredTaxType = computed(() => {
|
||||
if (textSearch.value) {
|
||||
return taxTypeStore.taxTypes.filter(function (el) {
|
||||
return (
|
||||
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return taxTypeStore.taxTypes
|
||||
}
|
||||
})
|
||||
|
||||
const taxes = computed(() => {
|
||||
return props.store[props.storeProp].taxes
|
||||
})
|
||||
|
||||
function selectTaxType(data, close) {
|
||||
emit('select:taxType', { ...data })
|
||||
close()
|
||||
}
|
||||
|
||||
function openTaxTypeModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: (data) => emit('select:taxType', data),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="flex text-gray-800 font-medium text-sm mb-2">
|
||||
{{ $t('general.select_template') }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</label>
|
||||
<BaseButton
|
||||
type="button"
|
||||
class="flex justify-center w-full text-sm lg:w-auto hover:bg-gray-200"
|
||||
variant="gray"
|
||||
@click="openTemplateModal"
|
||||
>
|
||||
<template #right="slotProps">
|
||||
<BaseIcon name="PencilIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ store[storeProp].template_name }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function openTemplateModal() {
|
||||
modalStore.openModal({
|
||||
title: t('general.choose_template'),
|
||||
componentName: 'SelectTemplate',
|
||||
data: {
|
||||
templates: props.store.templates,
|
||||
store: props.store,
|
||||
storeProp: props.storeProp,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="onCancel" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="createNewBackup">
|
||||
<div class="p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.backup.select_backup_type')"
|
||||
:error="
|
||||
v$.currentBackupData.option.$error &&
|
||||
v$.currentBackupData.option.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.option"
|
||||
:options="options"
|
||||
:can-deselect="false"
|
||||
:placeholder="$t('settings.backup.select_backup_type')"
|
||||
searchable
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.select_disk')"
|
||||
:error="
|
||||
v$.currentBackupData.selected_disk.$error &&
|
||||
v$.currentBackupData.selected_disk.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.selected_disk"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="getDisksOptions"
|
||||
:searchable="true"
|
||||
:allow-empty="false"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:placeholder="$t('settings.disk.select_disk')"
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isCreateLoading"
|
||||
:disabled="isCreateLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isCreateLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.create') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBackupStore } from '@/scripts/admin/stores/backup'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
let table = ref(null)
|
||||
let isSaving = ref(false)
|
||||
let isCreateLoading = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
const options = reactive(['full', 'only-db', 'only-files'])
|
||||
|
||||
const backupStore = useBackupStore()
|
||||
const modalStore = useModalStore()
|
||||
const diskStore = useDiskStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'BackupModal'
|
||||
})
|
||||
|
||||
const getDisksOptions = computed(() => {
|
||||
return diskStore.disks.map((disk) => {
|
||||
return {
|
||||
...disk,
|
||||
name: disk.name + ' — ' + '[' + disk.driver + ']',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentBackupData: {
|
||||
option: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_disk: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => backupStore)
|
||||
)
|
||||
|
||||
async function createNewBackup() {
|
||||
v$.value.currentBackupData.$touch()
|
||||
if (v$.value.currentBackupData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
option: backupStore.currentBackupData.option,
|
||||
file_disk_id: backupStore.currentBackupData.selected_disk.id,
|
||||
}
|
||||
try {
|
||||
isCreateLoading.value = true
|
||||
let res = await backupStore.createBackup(data)
|
||||
if (res.data) {
|
||||
isCreateLoading.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
modalStore.closeModal()
|
||||
}
|
||||
} catch (e) {
|
||||
isCreateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isFetchingInitialData.value = true
|
||||
let res = await diskStore.fetchDisks({ limit: 'all' })
|
||||
backupStore.currentBackupData.selected_disk = res.data.data[0]
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
backupStore.$reset()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCategoryModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCategoryData">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.category')"
|
||||
:error="
|
||||
v$.currentCategory.name.$error &&
|
||||
v$.currentCategory.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="categoryStore.currentCategory.name"
|
||||
:invalid="v$.currentCategory.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentCategory.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.description')"
|
||||
:error="
|
||||
v$.currentCategory.description.$error &&
|
||||
v$.currentCategory.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="categoryStore.currentCategory.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentCategory.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeCategoryModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ categoryStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCategoryStore } from '@/scripts/admin/stores/category'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const categoryStore = useCategoryStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCategory: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => categoryStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CategoryModal'
|
||||
})
|
||||
|
||||
async function submitCategoryData() {
|
||||
v$.value.currentCategory.$touch()
|
||||
|
||||
if (v$.value.currentCategory.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const action = categoryStore.isEdit
|
||||
? categoryStore.updateCategory
|
||||
: categoryStore.addCategory
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(categoryStore.currentCategory)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCategoryModal()
|
||||
}
|
||||
|
||||
function closeCategoryModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
categoryStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal" @open="getInitials">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCompanyModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 mb-16 sm:p-6 space-y-4">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.company_logo')"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isFetchingInitialData">
|
||||
<BaseContentPlaceholdersBox :rounded="true" class="w-full h-24" />
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<BaseFileUploader
|
||||
:preview-image="previewLogo"
|
||||
base64
|
||||
@remove="onFileInputRemove"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.company_info.company_name')"
|
||||
:error="
|
||||
v$.newCompanyForm.name.$error &&
|
||||
v$.newCompanyForm.name.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="newCompanyForm.name"
|
||||
:invalid="v$.newCompanyForm.name.$error"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.newCompanyForm.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.country')"
|
||||
:error="
|
||||
v$.newCompanyForm.address.country_id.$error &&
|
||||
v$.newCompanyForm.address.country_id.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.address.country_id"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="name"
|
||||
:invalid="v$.newCompanyForm.address.country_id.$error"
|
||||
:options="globalStore.countries"
|
||||
value-prop="id"
|
||||
:can-deselect="true"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.currency')"
|
||||
:error="
|
||||
v$.newCompanyForm.currency.$error &&
|
||||
v$.newCompanyForm.currency.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:help-text="$t('wizard.currency_set_alert')"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.currency"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.currencies"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:searchable="true"
|
||||
track-by="name"
|
||||
:placeholder="$tc('settings.currencies.select_currency')"
|
||||
:invalid="v$.newCompanyForm.currency.$error"
|
||||
class="w-full"
|
||||
>
|
||||
</BaseMultiselect>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
let previewLogo = ref(null)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let companyLogoFileBlob = ref(null)
|
||||
let companyLogoName = ref(null)
|
||||
|
||||
const newCompanyForm = reactive({
|
||||
name: null,
|
||||
currency: '',
|
||||
address: {
|
||||
country_id: null,
|
||||
},
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
newCompanyForm: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
address: {
|
||||
country_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currency: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { newCompanyForm })
|
||||
|
||||
async function getInitials() {
|
||||
isFetchingInitialData.value = true
|
||||
await globalStore.fetchCurrencies()
|
||||
await globalStore.fetchCountries()
|
||||
|
||||
newCompanyForm.currency = companyStore.selectedCompanyCurrency.id
|
||||
newCompanyForm.address.country_id =
|
||||
companyStore.selectedCompany.address.country_id
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onFileInputChange(fileName, file) {
|
||||
companyLogoName.value = fileName
|
||||
companyLogoFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
companyLogoName.value = null
|
||||
companyLogoFileBlob.value = null
|
||||
}
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.newCompanyForm.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const res = await companyStore.addNewCompany(newCompanyForm)
|
||||
if (res.data.data) {
|
||||
await companyStore.setSelectedCompany(res.data.data)
|
||||
if (companyLogoFileBlob && companyLogoFileBlob.value) {
|
||||
let logoData = new FormData()
|
||||
|
||||
logoData.append(
|
||||
'company_logo',
|
||||
JSON.stringify({
|
||||
name: companyLogoName.value,
|
||||
data: companyLogoFileBlob.value,
|
||||
})
|
||||
)
|
||||
|
||||
await companyStore.updateCompanyLogo(logoData)
|
||||
router.push('/admin/dashboard')
|
||||
}
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
closeCompanyModal()
|
||||
}
|
||||
isSaving.value = false
|
||||
} catch {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
newCompanyForm.name = ''
|
||||
newCompanyForm.currency = ''
|
||||
newCompanyForm.address.country_id = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,665 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeCustomerModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomerModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCustomerData">
|
||||
<div class="px-6 pb-3">
|
||||
<BaseTabGroup>
|
||||
<BaseTab :title="$t('customers.basic_info')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.display_name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.currencies.currency')"
|
||||
required
|
||||
:error="
|
||||
v$.currency_id.$error && v$.currency_id.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.currency_id"
|
||||
:options="globalStore.currencies"
|
||||
value-prop="id"
|
||||
searchable
|
||||
:placeholder="$t('customers.select_currency')"
|
||||
:max-height="200"
|
||||
class="mt-1 md:mt-0"
|
||||
track-by="name"
|
||||
:invalid="v$.currency_id.$error"
|
||||
label="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.primary_contact_name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.contact_name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('login.email')"
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.email"
|
||||
type="text"
|
||||
name="email"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.prefix')"
|
||||
:error="v$.prefix.$error && v$.prefix.$errors[0].$message"
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.prefix"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="name"
|
||||
class=""
|
||||
:invalid="v$.prefix.$error"
|
||||
@input="v$.prefix.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.website')"
|
||||
:error="v$.website.$error && v$.website.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.website"
|
||||
type="url"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.website.$error"
|
||||
@input="v$.website.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.portal_access')">
|
||||
<BaseInputGrid class="col-span-5 lg:col-span-4">
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ $t('customers.portal_access_text') }}
|
||||
</p>
|
||||
|
||||
<BaseSwitch
|
||||
v-model="customerStore.currentCustomer.enable_portal"
|
||||
class="mt-1 flex"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('customers.portal_access_url')"
|
||||
class="md:col-span-2"
|
||||
:help-text="$t('customers.portal_access_url_help')"
|
||||
>
|
||||
<CopyInputField :token="getCustomerPortalUrl" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
:label="$t('customers.password')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.password"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
:invalid="v$.password.$error"
|
||||
@input="v$.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
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:error="
|
||||
v$.confirm_password.$error &&
|
||||
v$.confirm_password.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="Confirm Password"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.confirm_password"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="isShowConfirmPassword ? 'text' : 'password'"
|
||||
name="confirm_password"
|
||||
:invalid="v$.confirm_password.$error"
|
||||
@input="v$.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>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.billing_address')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.billing.country_id"
|
||||
:options="globalStore.countries"
|
||||
searchable
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
:allow-empty="false"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.state"
|
||||
type="text"
|
||||
name="billingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.city"
|
||||
type="text"
|
||||
name="billingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.billing.address_street_1.$error &&
|
||||
v$.billing.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.billing.address_street_1.$error"
|
||||
@input="v$.billing.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.billing.address_street_2.$error &&
|
||||
v$.billing.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.billing.address_street_2.$error"
|
||||
@input="v$.billing.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.billing.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.shipping_address')" class="!mt-2">
|
||||
<div class="grid md:grid-cols-12">
|
||||
<div class="flex justify-end col-span-12">
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="xs"
|
||||
@click="copyAddress(true)"
|
||||
>
|
||||
{{ $t('customers.copy_billing_address') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.shipping.country_id"
|
||||
:options="globalStore.countries"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.state"
|
||||
type="text"
|
||||
name="shippingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.city"
|
||||
type="text"
|
||||
name="shippingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.shipping.address_street_1.$error &&
|
||||
v$.shipping.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.shipping.address_street_2.$error &&
|
||||
v$.shipping.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.shipping.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
</BaseTabGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomerModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isLoading" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
email,
|
||||
alpha,
|
||||
url,
|
||||
helpers,
|
||||
requiredIf,
|
||||
sameAs,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import CopyInputField from '@/scripts/admin/components/CopyInputField.vue'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
|
||||
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const customerStore = useCustomerStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const isEdit = ref(false)
|
||||
const isLoading = ref(false)
|
||||
let isShowPassword = ref(false)
|
||||
let isShowConfirmPassword = ref(false)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'CustomerModal'
|
||||
)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
currency_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(
|
||||
customerStore.currentCustomer.enable_portal == true &&
|
||||
!customerStore.currentCustomer.password_added
|
||||
)
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.password_min_length', { count: 8 }),
|
||||
minLength(8)
|
||||
),
|
||||
},
|
||||
confirm_password: {
|
||||
sameAsPassword: helpers.withMessage(
|
||||
t('validation.password_incorrect'),
|
||||
sameAs(customerStore.currentCustomer.password)
|
||||
),
|
||||
},
|
||||
email: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(customerStore.currentCustomer.enable_portal == true)
|
||||
),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
prefix: {
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
website: {
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
|
||||
billing: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
shipping: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customerStore.currentCustomer)
|
||||
)
|
||||
|
||||
const getCustomerPortalUrl = computed(() => {
|
||||
return `${window.location.origin}/${companyStore.selectedCompany.slug}/customer/login`
|
||||
})
|
||||
|
||||
function copyAddress() {
|
||||
customerStore.copyAddress()
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
if (!customerStore.isEdit) {
|
||||
customerStore.currentCustomer.currency_id =
|
||||
companyStore.selectedCompanyCurrency.id
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomerData() {
|
||||
if (customerStore.currentCustomer.email === '') {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('settings.notification.please_enter_email'),
|
||||
})
|
||||
}
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
let data = {
|
||||
...customerStore.currentCustomer,
|
||||
}
|
||||
|
||||
try {
|
||||
let response = null
|
||||
if (customerStore.isEdit) {
|
||||
response = await customerStore.updateCustomer(data)
|
||||
} else {
|
||||
response = await customerStore.addCustomer(data)
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
isLoading.value = false
|
||||
// Automatically create newly created customer
|
||||
if (route.name === 'invoices.create' || route.name === 'invoices.edit') {
|
||||
invoiceStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
if (
|
||||
route.name === 'estimates.create' ||
|
||||
route.name === 'estimates.edit'
|
||||
) {
|
||||
estimateStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
if (
|
||||
route.name === 'recurring-invoices.create' ||
|
||||
route.name === 'recurring-invoices.edit'
|
||||
) {
|
||||
recurringInvoiceStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
closeCustomerModal()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomerModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
customerStore.resetCurrentCustomer()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="px-6 pt-6">
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ modalStore.title }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{
|
||||
$t('settings.company_info.delete_company_modal_desc', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 sm:p-6 space-y-4">
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.company_info.delete_company_modal_label', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
"
|
||||
:error="
|
||||
v$.formData.name.$error && v$.formData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.name"
|
||||
:invalid="v$.formData.name.$error"
|
||||
@input="v$.formData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isDeleting"
|
||||
name="TrashIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers, sameAs } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
let isDeleting = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
id: companyStore.selectedCompany.id,
|
||||
name: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'DeleteCompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
sameAsName: helpers.withMessage(
|
||||
t('validation.company_name_not_same'),
|
||||
sameAs(companyStore.selectedCompany.name)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
{ formData },
|
||||
{
|
||||
$scope: false,
|
||||
}
|
||||
)
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const company = companyStore.companies[0]
|
||||
|
||||
isDeleting.value = true
|
||||
try {
|
||||
const res = await companyStore.deleteCompany(formData)
|
||||
console.log(res.data.success)
|
||||
if (res.data.success) {
|
||||
closeCompanyModal()
|
||||
await companyStore.setSelectedCompany(company)
|
||||
router.push('/admin/dashboard')
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
}
|
||||
isDeleting.value = false
|
||||
} catch {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
formData.id = null
|
||||
formData.name = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive">
|
||||
<ExchangeRateBulkUpdate @update="closeModal()" />
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ExchangeRateBulkUpdate from '@/scripts/admin/components/currency-exchange-rate/ExchangeRateBulkUpdate.vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateBulkUpdateModal'
|
||||
)
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,482 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeExchangeRateModal"
|
||||
@open="fetchInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeExchangeRateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitExchangeRate">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.exchange_rate.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
:error="
|
||||
v$.currentExchangeRate.driver.$error &&
|
||||
v$.currentExchangeRate.driver.$errors[0].$message
|
||||
"
|
||||
:help-text="driverSite"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.driver"
|
||||
:options="driversLists"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
:can-deselect="true"
|
||||
label="key"
|
||||
:searchable="true"
|
||||
:invalid="v$.currentExchangeRate.driver.$error"
|
||||
@update:modelValue="resetCurrency"
|
||||
@input="v$.currentExchangeRate.driver.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
v-if="isCurrencyConverter"
|
||||
required
|
||||
:label="$t('settings.exchange_rate.server')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.type.$error &&
|
||||
v$.currencyConverter.type.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currencyConverter.type"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
searchable
|
||||
:options="serverOptions"
|
||||
:invalid="v$.currencyConverter.type.$error"
|
||||
label="value"
|
||||
@update:modelValue="resetCurrency"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.exchange_rate.key')"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.key.$error &&
|
||||
v$.currentExchangeRate.key.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currentExchangeRate.key"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="key"
|
||||
:loading="isFetchingCurrencies"
|
||||
loading-position="right"
|
||||
:invalid="v$.currentExchangeRate.key.$error"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="exchangeRateStore.supportedCurrencies.length"
|
||||
:label="$t('settings.exchange_rate.currency')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.currencies.$error &&
|
||||
v$.currentExchangeRate.currencies.$errors[0].$message
|
||||
"
|
||||
:help-text="$t('settings.exchange_rate.currency_help_text')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.currencies"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="code"
|
||||
mode="tags"
|
||||
searchable
|
||||
:options="exchangeRateStore.supportedCurrencies"
|
||||
:invalid="v$.currentExchangeRate.currencies.$error"
|
||||
label="code"
|
||||
track-by="code"
|
||||
@input="v$.currentExchangeRate.currencies.$touch()"
|
||||
openDirection="top"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<!-- For Currency Converter -->
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDedicatedServer"
|
||||
:label="$t('settings.exchange_rate.url')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.url.$error &&
|
||||
v$.currencyConverter.url.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currencyConverter.url"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="url"
|
||||
:invalid="v$.currencyConverter.url.$error"
|
||||
@input="v$.currencyConverter.url.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseSwitch
|
||||
v-model="exchangeRateStore.currentExchangeRate.active"
|
||||
class="flex"
|
||||
:label-right="$t('settings.exchange_rate.active')"
|
||||
/>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInfoAlert
|
||||
v-if="
|
||||
currenciesAlredayInUsed.length &&
|
||||
exchangeRateStore.currentExchangeRate.active
|
||||
"
|
||||
class="mt-5"
|
||||
:title="$t('settings.exchange_rate.currency_in_used')"
|
||||
:lists="[currenciesAlredayInUsed.toString()]"
|
||||
:actions="['Remove']"
|
||||
@hide="dismiss"
|
||||
@Remove="removeUsedSelectedCurrencies"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
:disabled="isSaving"
|
||||
@click="closeExchangeRateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving || isFetchingCurrencies"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
exchangeRateStore.isEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
helpers,
|
||||
requiredIf,
|
||||
url,
|
||||
} from '@vuelidate/validators'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let isFetchingCurrencies = ref(false)
|
||||
let currenciesAlredayInUsed = ref([])
|
||||
let currenctPorivderOldCurrencies = ref([])
|
||||
const modalStore = useModalStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
|
||||
let serverOptions = ref([])
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentExchangeRate: {
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
currencies: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currencyConverter: {
|
||||
type: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isCurrencyConverter)
|
||||
),
|
||||
},
|
||||
url: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isDedicatedServer)
|
||||
),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
const driversLists = computed(() => {
|
||||
return exchangeRateStore.drivers.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
key: t(item.key),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateProviderModal'
|
||||
)
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const isCurrencyConverter = computed(() => {
|
||||
return exchangeRateStore.currentExchangeRate.driver === 'currency_converter'
|
||||
})
|
||||
|
||||
const isDedicatedServer = computed(() => {
|
||||
return (
|
||||
exchangeRateStore.currencyConverter &&
|
||||
exchangeRateStore.currencyConverter.type === 'DEDICATED'
|
||||
)
|
||||
})
|
||||
|
||||
const driverSite = computed(() => {
|
||||
switch (exchangeRateStore.currentExchangeRate.driver) {
|
||||
case 'currency_converter':
|
||||
return `https://www.currencyconverterapi.com`
|
||||
|
||||
case 'currency_freak':
|
||||
return 'https://currencyfreaks.com'
|
||||
|
||||
case 'currency_layer':
|
||||
return 'https://currencylayer.com'
|
||||
|
||||
case 'open_exchange_rate':
|
||||
return 'https://openexchangerates.org'
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => exchangeRateStore)
|
||||
)
|
||||
|
||||
function dismiss() {
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
function removeUsedSelectedCurrencies() {
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
currenciesAlredayInUsed.value.forEach((uc) => {
|
||||
currencies.forEach((c, i) => {
|
||||
if (c === uc) {
|
||||
currencies.splice(i, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
function resetCurrency() {
|
||||
exchangeRateStore.currentExchangeRate.key = null
|
||||
exchangeRateStore.currentExchangeRate.currencies = []
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
}
|
||||
|
||||
function resetModalData() {
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
currenctPorivderOldCurrencies.value = []
|
||||
exchangeRateStore.currentExchangeRate = {
|
||||
id: null,
|
||||
name: '',
|
||||
driver: '',
|
||||
key: '',
|
||||
active: true,
|
||||
currencies: [],
|
||||
}
|
||||
|
||||
exchangeRateStore.currencyConverter = {
|
||||
type: '',
|
||||
url: '',
|
||||
}
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
async function fetchInitialData() {
|
||||
exchangeRateStore.currentExchangeRate.driver = 'currency_converter'
|
||||
let params = {}
|
||||
if (exchangeRateStore.isEdit) {
|
||||
params.provider_id = exchangeRateStore.currentExchangeRate.id
|
||||
}
|
||||
isFetchingInitialData.value = true
|
||||
await exchangeRateStore.fetchDefaultProviders()
|
||||
await exchangeRateStore.fetchActiveCurrency(params)
|
||||
|
||||
currenctPorivderOldCurrencies.value =
|
||||
exchangeRateStore.currentExchangeRate.currencies
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCurrencyConverter.value,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchServers()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore.currentExchangeRate.key,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore?.currencyConverter?.type,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fetchCurrencies = debounce(fetchCurrencies, 500)
|
||||
|
||||
function validate() {
|
||||
v$.value.$touch()
|
||||
checkingIsActiveCurrencies()
|
||||
if (
|
||||
v$.value.$invalid ||
|
||||
(currenciesAlredayInUsed.value.length &&
|
||||
exchangeRateStore.currentExchangeRate.active)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function submitExchangeRate() {
|
||||
if (validate()) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
...exchangeRateStore.currentExchangeRate,
|
||||
}
|
||||
if (isCurrencyConverter.value) {
|
||||
data.driver_config = {
|
||||
...exchangeRateStore.currencyConverter,
|
||||
}
|
||||
if (!isDedicatedServer.value) {
|
||||
data.driver_config.url = ''
|
||||
}
|
||||
}
|
||||
const action = exchangeRateStore.isEdit
|
||||
? exchangeRateStore.updateProvider
|
||||
: exchangeRateStore.addProvider
|
||||
isSaving.value = true
|
||||
|
||||
await action(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeExchangeRateModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchServers() {
|
||||
let res = await exchangeRateStore.getCurrencyConverterServers()
|
||||
serverOptions.value = res.data.currency_converter_servers
|
||||
exchangeRateStore.currencyConverter.type = 'FREE'
|
||||
}
|
||||
|
||||
function fetchCurrencies() {
|
||||
const { driver, key } = exchangeRateStore.currentExchangeRate
|
||||
if (driver && key) {
|
||||
isFetchingCurrencies.value = true
|
||||
let data = {
|
||||
driver: driver,
|
||||
key: key,
|
||||
}
|
||||
if (
|
||||
isCurrencyConverter.value &&
|
||||
!exchangeRateStore.currencyConverter.type
|
||||
) {
|
||||
isFetchingCurrencies.value = false
|
||||
return
|
||||
}
|
||||
if (exchangeRateStore?.currencyConverter?.type) {
|
||||
data.type = exchangeRateStore.currencyConverter.type
|
||||
}
|
||||
|
||||
exchangeRateStore
|
||||
.fetchCurrencies(data)
|
||||
.then((res) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function checkingIsActiveCurrencies(showError = true) {
|
||||
currenciesAlredayInUsed.value = []
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
|
||||
if (currencies.length && exchangeRateStore.activeUsedCurrencies?.length) {
|
||||
currencies.forEach((curr) => {
|
||||
if (exchangeRateStore.activeUsedCurrencies.includes(curr)) {
|
||||
currenciesAlredayInUsed.value.push(curr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeExchangeRateModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
resetModalData()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeDiskModal" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeDiskModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-disk-modal">
|
||||
<component
|
||||
:is="diskStore.selected_driver"
|
||||
:loading="isLoading"
|
||||
:disks="diskStore.getDiskDrivers"
|
||||
:is-edit="isEdit"
|
||||
@onChangeDisk="(val) => diskChange(val)"
|
||||
@submit="createNewDisk"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeDiskModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isRequestFire(slotProps)"
|
||||
:disabled="isRequestFire(slotProps)"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isRequestFire(slotProps)"
|
||||
name="SaveIcon"
|
||||
class="w-6 mr-2"
|
||||
/>
|
||||
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import Dropbox from '@/scripts/admin/components/modal-components/disks/DropboxDisk.vue'
|
||||
import Local from '@/scripts/admin/components/modal-components/disks/LocalDisk.vue'
|
||||
import S3 from '@/scripts/admin/components/modal-components/disks/S3Disk.vue'
|
||||
import DoSpaces from '@/scripts/admin/components/modal-components/disks/DoSpacesDisk.vue'
|
||||
export default {
|
||||
components: {
|
||||
Dropbox,
|
||||
Local,
|
||||
S3,
|
||||
DoSpaces,
|
||||
},
|
||||
setup() {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let isEdit = ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
if (modalStore.id) {
|
||||
isEdit.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'FileDiskModal'
|
||||
})
|
||||
|
||||
function isRequestFire(slotProps) {
|
||||
return (
|
||||
slotProps && (slotProps.diskData.isLoading.value || isLoading.value)
|
||||
)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let res = await diskStore.fetchDiskDrivers()
|
||||
if (isEdit.value) {
|
||||
diskStore.selected_driver = modalStore.data.driver
|
||||
} else {
|
||||
diskStore.selected_driver = res.data.drivers[0].value
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
async function createNewDisk(data) {
|
||||
Object.assign(diskStore.diskConfigData, data)
|
||||
isLoading.value = true
|
||||
|
||||
let formData = {
|
||||
id: modalStore.id,
|
||||
...data,
|
||||
}
|
||||
|
||||
let response = null
|
||||
const action = isEdit.value ? diskStore.updateDisk : diskStore.createDisk
|
||||
response = await action(formData)
|
||||
isLoading.value = false
|
||||
modalStore.refreshData()
|
||||
closeDiskModal()
|
||||
}
|
||||
|
||||
function closeDiskModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
|
||||
function diskChange(value) {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.diskConfigData.selected_driver = value
|
||||
}
|
||||
|
||||
return {
|
||||
isEdit,
|
||||
createNewDisk,
|
||||
isRequestFire,
|
||||
diskStore,
|
||||
closeDiskModal,
|
||||
loadData,
|
||||
diskChange,
|
||||
modalStore,
|
||||
isLoading,
|
||||
modalActive,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeItemModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="item-modal">
|
||||
<form action="" @submit.prevent="submitItemData">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('items.name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItem.name"
|
||||
type="text"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.price')">
|
||||
<BaseMoney
|
||||
:key="companyStore.selectedCompanyCurrency"
|
||||
v-model="price"
|
||||
:currency="companyStore.selectedCompanyCurrency"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.unit')">
|
||||
<BaseMultiselect
|
||||
v-model="itemStore.currentItem.unit_id"
|
||||
label="name"
|
||||
:options="itemStore.itemUnits"
|
||||
value-prop="id"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
searchable
|
||||
track-by="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isTaxPerItemEnabled"
|
||||
:label="$t('items.taxes')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="taxes"
|
||||
:options="getTaxTypes"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
class="w-full"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('items.description')"
|
||||
:error="
|
||||
v$.description.$error && v$.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="itemStore.currentItem.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.description.$error"
|
||||
@input="v$.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeItemModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ itemStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
minValue,
|
||||
helpers,
|
||||
alpha,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
|
||||
const emit = defineEmits(['newItem'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const itemStore = useItemStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const taxPerItemSetting = ref(companyStore.selectedCompanySettings.tax_per_item)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'ItemModal'
|
||||
)
|
||||
|
||||
const price = computed({
|
||||
get: () => itemStore.currentItem.price / 100,
|
||||
set: (value) => {
|
||||
itemStore.currentItem.price = Math.round(value * 100)
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () =>
|
||||
itemStore.currentItem.taxes.map((tax) => {
|
||||
if (tax) {
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: tax.name + ' (' + tax.percent + '%)',
|
||||
}
|
||||
}
|
||||
}),
|
||||
set: (value) => {
|
||||
itemStore.$patch((state) => {
|
||||
state.currentItem.taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const isTaxPerItemEnabled = computed(() => {
|
||||
return taxPerItemSetting.value === 'YES'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItem)
|
||||
)
|
||||
|
||||
const getTaxTypes = computed(() => {
|
||||
return taxTypeStore.taxTypes.map((tax) => {
|
||||
return { ...tax, tax_name: tax.name + ' (' + tax.percent + '%)' }
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
v$.value.$reset()
|
||||
itemStore.fetchItemUnits({ limit: 'all' })
|
||||
})
|
||||
|
||||
async function submitItemData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
...itemStore.currentItem,
|
||||
taxes: itemStore.currentItem.taxes.map((tax) => {
|
||||
return {
|
||||
tax_type_id: tax.id,
|
||||
amount: (price.value * tax.percent) / 100,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
collective_tax: 0,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const action = itemStore.isEdit ? itemStore.updateItem : itemStore.addItem
|
||||
|
||||
await action(data).then((res) => {
|
||||
isLoading.value = false
|
||||
if (res.data.data) {
|
||||
if (modalStore.data) {
|
||||
modalStore.refreshData(res.data.data)
|
||||
}
|
||||
}
|
||||
closeItemModal()
|
||||
})
|
||||
}
|
||||
|
||||
function closeItemModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
itemStore.resetCurrentItem()
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'ItemUnitModal'"
|
||||
@close="closeItemUnitModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemUnitModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitItemUnit">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.items.unit_name')"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItemUnit.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeItemUnitModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
itemStore.isItemUnitEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const itemStore = useItemStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItemUnit)
|
||||
)
|
||||
|
||||
async function submitItemUnit() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = itemStore.isItemUnitEdit
|
||||
? itemStore.updateItemUnit
|
||||
: itemStore.addItemUnit
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(itemStore.currentItemUnit)
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeItemUnitModal()
|
||||
isSaving.value = false
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closeItemUnitModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
itemStore.currentItemUnit = {
|
||||
id: null,
|
||||
name: '',
|
||||
}
|
||||
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeTestModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTestModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="onTestMailSend">
|
||||
<div class="p-4 md:p-8">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
:error="v$.formData.to.$error && v$.formData.to.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
ref="to"
|
||||
v-model="formData.to"
|
||||
type="text"
|
||||
:invalid="v$.formData.to.$error"
|
||||
@input="v$.formData.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
:error="
|
||||
v$.formData.subject.$error &&
|
||||
v$.formData.subject.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.subject"
|
||||
type="text"
|
||||
:invalid="v$.formData.subject.$error"
|
||||
@input="v$.formData.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.message')"
|
||||
:error="
|
||||
v$.formData.message.$error &&
|
||||
v$.formData.message.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="formData.message"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.formData.message.$error"
|
||||
@input="v$.formData.message.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="closeTestModal()"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isSaving" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="PaperAirplaneIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, maxLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
let isSaving = ref(false)
|
||||
let formData = reactive({
|
||||
to: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'MailTestModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.subject_maxlength'),
|
||||
maxLength(100)
|
||||
),
|
||||
},
|
||||
message: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.message_maxlength'),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { formData })
|
||||
|
||||
function resetFormData() {
|
||||
formData.id = ''
|
||||
formData.to = ''
|
||||
formData.subject = ''
|
||||
formData.message = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
async function onTestMailSend() {
|
||||
v$.value.formData.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
let response = await mailDriverStore.sendTestMail(formData)
|
||||
if (response.data) {
|
||||
closeTestModal()
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
function closeTestModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
modalStore.resetModalData()
|
||||
resetFormData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeNoteModal" @open="setFields">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeNoteModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitNote">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.name')"
|
||||
variant="vertical"
|
||||
:error="
|
||||
v$.currentNote.name.$error &&
|
||||
v$.currentNote.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="noteStore.currentNote.name"
|
||||
:invalid="v$.currentNote.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentNote.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.type')"
|
||||
:error="
|
||||
v$.currentNote.type.$error &&
|
||||
v$.currentNote.type.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="noteStore.currentNote.type"
|
||||
:options="types"
|
||||
value-prop="type"
|
||||
class="mt-2"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.notes')"
|
||||
:error="
|
||||
v$.currentNote.notes.$error &&
|
||||
v$.currentNote.notes.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="noteStore.currentNote.notes"
|
||||
:invalid="v$.currentNote.notes.$error"
|
||||
:fields="fields"
|
||||
@input="v$.currentNote.notes.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
px-4
|
||||
py-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeNoteModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ noteStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const noteStore = useNotesStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
const types = reactive(['Invoice', 'Estimate', 'Payment'])
|
||||
let fields = ref(['customer', 'customerCustom'])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'NoteModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentNote: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
notes: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => noteStore)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => noteStore.currentNote.type,
|
||||
(val) => {
|
||||
setFields()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name === 'estimates.create') {
|
||||
noteStore.currentNote.type = 'Estimate'
|
||||
} else if (route.name === 'invoices.create') {
|
||||
noteStore.currentNote.type = 'Invoice'
|
||||
} else {
|
||||
noteStore.currentNote.type = 'Payment'
|
||||
}
|
||||
})
|
||||
|
||||
function setFields() {
|
||||
fields.value = ['customer', 'customerCustom']
|
||||
|
||||
if (noteStore.currentNote.type == 'Invoice') {
|
||||
fields.value.push('invoice', 'invoiceCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Estimate') {
|
||||
fields.value.push('estimate', 'estimateCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Payment') {
|
||||
fields.value.push('payment', 'paymentCustom')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNote() {
|
||||
v$.value.currentNote.$touch()
|
||||
if (v$.value.currentNote.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
if (noteStore.isEdit) {
|
||||
let data = {
|
||||
id: noteStore.currentNote.id,
|
||||
...noteStore.currentNote,
|
||||
}
|
||||
await noteStore
|
||||
.updateNote(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_updated'),
|
||||
})
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
} else {
|
||||
await noteStore
|
||||
.addNote(noteStore.currentNote)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_added'),
|
||||
})
|
||||
|
||||
if (
|
||||
(route.name === 'invoices.create' &&
|
||||
res.data.data.type === 'Invoice') ||
|
||||
(route.name === 'invoices.edit' && res.data.data.type === 'Invoice')
|
||||
) {
|
||||
invoiceStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'estimates.create' &&
|
||||
res.data.data.type === 'Estimate') ||
|
||||
(route.name === 'estimates.edit' &&
|
||||
res.data.data.type === 'Estimate')
|
||||
) {
|
||||
estimateStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'payments.create' &&
|
||||
res.data.data.type === 'Payment') ||
|
||||
(route.name === 'payments.edit' && res.data.data.type === 'Payment')
|
||||
) {
|
||||
paymentStore.selectNote(res.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeNoteModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
noteStore.resetCurrentNote()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.note-modal {
|
||||
.header-editior .editor-menu-bar {
|
||||
margin-left: 0.5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closePaymentModeModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closePaymentModeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitPaymentMode">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.payment_modes.mode_name')"
|
||||
:error="
|
||||
v$.currentPaymentMode.name.$error &&
|
||||
v$.currentPaymentMode.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentStore.currentPaymentMode.name"
|
||||
:invalid="v$.currentPaymentMode.name.$error"
|
||||
@input="v$.currentPaymentMode.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
class="mr-3"
|
||||
type="button"
|
||||
@click="closePaymentModeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{
|
||||
paymentStore.currentPaymentMode.id
|
||||
? $t('general.update')
|
||||
: $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentPaymentMode: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => paymentStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'PaymentModeModal'
|
||||
})
|
||||
|
||||
async function submitPaymentMode() {
|
||||
v$.value.currentPaymentMode.$touch()
|
||||
|
||||
if (v$.value.currentPaymentMode.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = paymentStore.currentPaymentMode.id
|
||||
? paymentStore.updatePaymentMode
|
||||
: paymentStore.addPaymentMode
|
||||
isSaving.value = true
|
||||
await action(paymentStore.currentPaymentMode)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closePaymentModeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closePaymentModeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
paymentStore.currentPaymentMode = {
|
||||
id: '',
|
||||
name: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeRolesModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeRolesModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitRoleData">
|
||||
<div class="px-4 md:px-8 py-4 md:py-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.roles.name')"
|
||||
class="mt-3"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="roleStore.currentRole.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<h6
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-gray-800
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
{{ $tc('settings.roles.permission', 2) }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</h6>
|
||||
<div
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-gray-300
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(true)"
|
||||
>
|
||||
{{ $t('settings.roles.select_all') }}
|
||||
</a>
|
||||
/
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(false)"
|
||||
>
|
||||
{{ $t('settings.roles.none') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 py-3">
|
||||
<div
|
||||
class="
|
||||
grid grid-cols-1
|
||||
sm:grid-cols-2
|
||||
md:grid-cols-3
|
||||
lg:grid-cols-4
|
||||
gap-4
|
||||
px-8
|
||||
sm:px-8
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(abilityGroup, gIndex) in roleStore.abilitiesList"
|
||||
:key="gIndex"
|
||||
class="flex flex-col space-y-1"
|
||||
>
|
||||
<p class="text-sm text-gray-500 border-b border-gray-200 pb-1 mb-2">
|
||||
{{ gIndex }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(ability, index) in abilityGroup"
|
||||
:key="index"
|
||||
class="flex"
|
||||
>
|
||||
<BaseCheckbox
|
||||
v-model="roleStore.currentRole.abilities"
|
||||
:set-initial-value="true"
|
||||
variant="primary"
|
||||
:disabled="ability.disabled"
|
||||
:label="ability.name"
|
||||
:value="ability"
|
||||
@update:modelValue="onUpdateAbility(ability)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="v$.abilities.$error"
|
||||
class="block mt-0.5 text-sm text-red-500"
|
||||
>
|
||||
{{ v$.abilities.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeRolesModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ !roleStore.isEdit ? $t('general.save') : $t('general.update') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useRoleStore } from '@/scripts/admin/stores/role'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const roleStore = useRoleStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'RolesModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
abilities: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.at_least_one_ability'),
|
||||
required
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => roleStore.currentRole)
|
||||
)
|
||||
|
||||
async function submitRoleData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = roleStore.isEdit ? roleStore.updateRole : roleStore.addRole
|
||||
isSaving.value = true
|
||||
await action(roleStore.currentRole)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeRolesModal()
|
||||
} catch (error) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateAbility(currentAbility) {
|
||||
const fd = roleStore.currentRole.abilities.find(
|
||||
(_abl) => _abl.ability === currentAbility.ability
|
||||
)
|
||||
|
||||
if (!fd && currentAbility?.depends_on?.length) {
|
||||
enableAbilities(currentAbility)
|
||||
return
|
||||
}
|
||||
|
||||
currentAbility?.depends_on?.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (_d === _a.ability) {
|
||||
_a.disabled = true
|
||||
|
||||
let found = roleStore.currentRole.abilities.find(
|
||||
(_af) => _af.ability === _d
|
||||
)
|
||||
|
||||
if (!found) {
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setSelectAll(checked) {
|
||||
let dependList = []
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a?.depends_on && (dependList = [...dependList, ..._a.depends_on])
|
||||
})
|
||||
})
|
||||
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (dependList.includes(_a.ability)) {
|
||||
checked ? (_a.disabled = true) : (_a.disabled = false)
|
||||
}
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
})
|
||||
})
|
||||
|
||||
if (!checked) roleStore.currentRole.abilities = []
|
||||
}
|
||||
|
||||
function enableAbilities(ability) {
|
||||
ability.depends_on.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
// CHECK IF EXISTS IN CURRENT ROLE ABILITIES
|
||||
let found = roleStore.currentRole.abilities.find((_r) =>
|
||||
_r.depends_on?.includes(_a.ability)
|
||||
)
|
||||
|
||||
if (_d === _a.ability && !found) {
|
||||
_a.disabled = false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function closeRolesModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
roleStore.currentRole = {
|
||||
id: null,
|
||||
name: '',
|
||||
abilities: [],
|
||||
}
|
||||
|
||||
// Enable all disabled ability
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a.disabled = false
|
||||
})
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<div
|
||||
v-if="modalStore.data"
|
||||
class="grid grid-cols-3 gap-2 p-1 overflow-x-auto"
|
||||
>
|
||||
<div
|
||||
v-for="(template, index) in modalStore.data.templates"
|
||||
:key="index"
|
||||
:class="{
|
||||
'border border-solid border-primary-500':
|
||||
selectedTemplate === template.name,
|
||||
}"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
m-2
|
||||
border border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:border-primary-300
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="template.path"
|
||||
:alt="template.name"
|
||||
class="w-full"
|
||||
@click="selectedTemplate = template.name"
|
||||
/>
|
||||
<img
|
||||
v-if="selectedTemplate === template.name"
|
||||
:alt="template.name"
|
||||
class="absolute z-10 w-5 h-5 text-primary-500"
|
||||
style="top: -6px; right: -5px"
|
||||
:src="getTickImage()"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
'w-full p-1 bg-gray-200 text-sm text-center absolute bottom-0 left-0',
|
||||
{
|
||||
'text-primary-500 bg-primary-100':
|
||||
selectedTemplate === template.name,
|
||||
'text-gray-600': selectedTemplate != template.name,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ template.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
|
||||
<BaseButton class="mr-3" variant="primary-outline" @click="closeModal">
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" @click="chooseTemplate()">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.choose') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const selectedTemplate = ref('')
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SelectTemplate'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
function setData() {
|
||||
if (modalStore.data.store[modalStore.data.storeProp].template_name) {
|
||||
selectedTemplate.value =
|
||||
modalStore.data.store[modalStore.data.storeProp].template_name
|
||||
} else {
|
||||
selectedTemplate.value = modalStore.data.templates[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseTemplate() {
|
||||
await modalStore.data.store.setTemplate(selectedTemplate.value)
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function getTickImage() {
|
||||
const imgUrl = new URL('/img/tick.png', import.meta.url)
|
||||
return imgUrl
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
modalStore.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendEstimateModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendEstimateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup :label="$t('general.body')" required>
|
||||
<BaseCustomInput
|
||||
v-model="estimateMailForm.body"
|
||||
:fields="estimateMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PhotographIcon" class="h-5 mr-2" />
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PaperAirplaneIcon" class="mr-2" />
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watchEffect, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const estimateMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimate',
|
||||
'estimateCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
let estimateMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Estimate',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendEstimateModal'
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => estimateMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
estimateMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
estimateMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
estimateMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
estimateMailForm.body =
|
||||
companyStore.selectedCompanySettings.estimate_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await estimateStore.previewEstimate(
|
||||
estimateMailForm
|
||||
)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await estimateStore.sendEstimate(estimateMailForm)
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendEstimateModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('estimates.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendEstimateModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendInvoiceModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendInvoiceModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="invoiceMailForm.body"
|
||||
:fields="invoiceMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const invoiceMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoice',
|
||||
'invoiceCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const invoiceMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Invoice',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendInvoiceModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => invoiceMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
invoiceMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
invoiceMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
invoiceMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
invoiceMailForm.body = companyStore.selectedCompanySettings.invoice_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await invoiceStore.previewInvoice(invoiceMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await invoiceStore.sendInvoice(invoiceMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendInvoiceModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('invoices.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendInvoiceModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendPaymentModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendPaymentModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="paymentMailForm.body"
|
||||
:fields="paymentMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="sendPaymentData"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="sendPaymentData()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { ref, reactive, computed, watch, watchEffect } from 'vue'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
|
||||
const paymentStore = usePaymentStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const mailDriversStore = useMailDriverStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const paymentMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'payments',
|
||||
'paymentsCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const paymentMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Payment',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendPaymentModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, paymentMailForm)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
paymentMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
paymentMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
paymentMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
paymentMailForm.body = companyStore.selectedCompanySettings.payment_mail_body
|
||||
}
|
||||
|
||||
async function sendPaymentData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await paymentStore.previewPayment(paymentMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await paymentStore.sendEmail(paymentMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendPaymentModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('payments.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendPaymentModal() {
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
modalStore.resetModalData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'TaxTypeModal'"
|
||||
@close="closeTaxTypeModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTaxTypeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitTaxTypeData">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.name')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.name.$error &&
|
||||
v$.currentTaxType.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="taxTypeStore.currentTaxType.name"
|
||||
:invalid="v$.currentTaxType.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentTaxType.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.percent')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.percent.$error &&
|
||||
v$.currentTaxType.percent.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMoney
|
||||
v-model="taxTypeStore.currentTaxType.percent"
|
||||
:currency="{
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
symbol: '% ',
|
||||
precision: 2,
|
||||
masked: false,
|
||||
}"
|
||||
:invalid="v$.currentTaxType.percent.$error"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
@input="v$.currentTaxType.percent.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.description')"
|
||||
:error="
|
||||
v$.currentTaxType.description.$error &&
|
||||
v$.currentTaxType.description.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="taxTypeStore.currentTaxType.description"
|
||||
:invalid="v$.currentTaxType.description.$error"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentTaxType.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.compound_tax')"
|
||||
variant="horizontal"
|
||||
class="flex flex-row-reverse"
|
||||
>
|
||||
<BaseSwitch
|
||||
v-model="taxTypeStore.currentTaxType.compound_tax"
|
||||
class="flex items-center"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeTaxTypeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ taxTypeStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/admin/stub/abilities'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
between,
|
||||
helpers,
|
||||
} from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentTaxType: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
percent: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
between: helpers.withMessage(
|
||||
t('validation.enter_valid_tax_rate'),
|
||||
between(0, 100)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => taxTypeStore)
|
||||
)
|
||||
|
||||
async function submitTaxTypeData() {
|
||||
v$.value.currentTaxType.$touch()
|
||||
if (v$.value.currentTaxType.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = taxTypeStore.isEdit
|
||||
? taxTypeStore.updateTaxType
|
||||
: taxTypeStore.addTaxType
|
||||
isSaving.value = true
|
||||
let res = await action(taxTypeStore.currentTaxType)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData(res.data.data) : ''
|
||||
closeTaxTypeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function SelectTax(taxData) {
|
||||
let amount = 0
|
||||
if (taxData.compound_tax && estimateStore.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((estimateStore.getSubtotalWithDiscount +
|
||||
estimateStore.getTotalSimpleTax) *
|
||||
taxData.percent) /
|
||||
100
|
||||
)
|
||||
} else if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
|
||||
amount = Math.round(
|
||||
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
|
||||
)
|
||||
}
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
amount,
|
||||
}
|
||||
estimateStore.$patch((state) => {
|
||||
state.newEstimate.taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function selectItemTax(taxData) {
|
||||
if (modalStore.data) {
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
}
|
||||
modalStore.refreshData(data)
|
||||
}
|
||||
}
|
||||
|
||||
function closeTaxTypeModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
taxTypeStore.resetCurrentTaxType()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setAddress">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex flex-col">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ modalStore.content }}
|
||||
</p>
|
||||
</div>
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="saveCustomerAddress">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.state.$error && v$.state.$errors[0].$message"
|
||||
:label="$t('customers.state')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.state"
|
||||
type="text"
|
||||
name="shippingState"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.state.$error"
|
||||
@input="v$.state.$touch()"
|
||||
:placeholder="$t('settings.taxations.state_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.city.$error && v$.city.$errors[0].$message"
|
||||
:label="$t('customers.city')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.city"
|
||||
type="text"
|
||||
name="shippingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.city.$error"
|
||||
@input="v$.city.$touch()"
|
||||
:placeholder="$t('settings.taxations.city_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="
|
||||
v$.address_street_1.$error &&
|
||||
v$.address_street_1.$errors[0].$message
|
||||
"
|
||||
:label="$t('customers.address')"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="address.address_street_1"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.address_street_1.$error"
|
||||
@input="v$.address_street_1.$touch()"
|
||||
:placeholder="$t('settings.taxations.address_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.zip.$error && v$.zip.$errors[0].$message"
|
||||
:label="$t('customers.zip_code')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.zip"
|
||||
:invalid="v$.zip.$error"
|
||||
@input="v$.zip.$touch()"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
:placeholder="$t('settings.taxations.zip_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isLoading" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { helpers, required } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const address = reactive({
|
||||
state: '',
|
||||
city: '',
|
||||
address_street_1: '',
|
||||
zip: '',
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'TaxationAddressModal'
|
||||
)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
state: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
city: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
address_street_1: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
zip: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => address)
|
||||
)
|
||||
|
||||
async function saveCustomerAddress() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
address,
|
||||
}
|
||||
if (modalStore.id) {
|
||||
data.customer_id = modalStore.id
|
||||
}
|
||||
// replace '/n' with empty string
|
||||
address.address_street_1 = address.address_street_1.replace(
|
||||
/(\r\n|\n|\r)/gm,
|
||||
''
|
||||
)
|
||||
|
||||
isLoading.value = true
|
||||
await taxTypeStore
|
||||
.fetchSalesTax(data)
|
||||
.then((res) => {
|
||||
isLoading.value = false
|
||||
emit('addTax', res.data.data)
|
||||
closeModal()
|
||||
})
|
||||
.catch((e) => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
const emit = defineEmits(['addTax'])
|
||||
|
||||
function setAddress() {
|
||||
address.state = modalStore?.data?.state
|
||||
address.city = modalStore?.data?.city
|
||||
address.address_street_1 = modalStore?.data?.address_street_1
|
||||
address.zip = modalStore?.data?.zip
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomFieldModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCustomFieldData">
|
||||
<div class="overflow-y-auto max-h-[550px]">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.name')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.name.$error &&
|
||||
v$.currentCustomField.name.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
ref="name"
|
||||
v-model="customFieldStore.currentCustomField.name"
|
||||
:invalid="v$.currentCustomField.name.$error"
|
||||
@input="v$.currentCustomField.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.model')"
|
||||
:error="
|
||||
v$.currentCustomField.model_type.$error &&
|
||||
v$.currentCustomField.model_type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.model_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customFieldStore.currentCustomField.model_type"
|
||||
:options="modelTypes"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.currentCustomField.model_type.$error"
|
||||
:searchable="true"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
@input="v$.currentCustomField.model_type.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
class="flex items-center space-x-4"
|
||||
:label="$t('settings.custom_fields.required')"
|
||||
>
|
||||
<BaseSwitch v-model="isRequiredField" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.type')"
|
||||
:error="
|
||||
v$.currentCustomField.type.$error &&
|
||||
v$.currentCustomField.type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.type_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selectedType"
|
||||
:options="dataTypes"
|
||||
:invalid="v$.currentCustomField.type.$error"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
:searchable="true"
|
||||
:can-deselect="false"
|
||||
object
|
||||
@update:modelValue="onSelectedTypeChange"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.label')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.label.$error &&
|
||||
v$.currentCustomField.label.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.label"
|
||||
:invalid="v$.currentCustomField.label.$error"
|
||||
@input="v$.currentCustomField.label.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDropdownSelected"
|
||||
:label="$t('settings.custom_fields.options')"
|
||||
>
|
||||
<OptionCreate @onAdd="addNewOption" />
|
||||
|
||||
<div
|
||||
v-for="(option, index) in customFieldStore.currentCustomField
|
||||
.options"
|
||||
:key="index"
|
||||
class="flex items-center mt-5"
|
||||
>
|
||||
<BaseInput v-model="option.name" class="w-64" />
|
||||
|
||||
<BaseIcon
|
||||
name="MinusCircleIcon"
|
||||
class="ml-1 cursor-pointer"
|
||||
:class="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? 'text-gray-300'
|
||||
: 'text-red-300'
|
||||
"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.default_value')"
|
||||
class="relative"
|
||||
>
|
||||
<component
|
||||
:is="defaultValueComponent"
|
||||
v-model="customFieldStore.currentCustomField.default_answer"
|
||||
:options="customFieldStore.currentCustomField.options"
|
||||
:default-date-time="
|
||||
customFieldStore.currentCustomField.dateTimeValue
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="!isSwitchTypeSelected"
|
||||
:label="$t('settings.custom_fields.placeholder')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.placeholder"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.order')"
|
||||
:error="
|
||||
v$.currentCustomField.order.$error &&
|
||||
v$.currentCustomField.order.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.order"
|
||||
type="number"
|
||||
:invalid="v$.currentCustomField.order.$error"
|
||||
@input="v$.currentCustomField.order.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomFieldModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="SaveIcon"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
!customFieldStore.isEdit ? $t('general.save') : $t('general.update')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed, defineAsyncComponent } from 'vue'
|
||||
import OptionCreate from './OptionsCreate.vue'
|
||||
import moment from 'moment'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, numeric, helpers } from '@vuelidate/validators'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const modelTypes = reactive([
|
||||
'Customer',
|
||||
'Invoice',
|
||||
'Estimate',
|
||||
'Expense',
|
||||
'Payment',
|
||||
])
|
||||
|
||||
const dataTypes = reactive([
|
||||
{ label: 'Text', value: 'Input' },
|
||||
{ label: 'Textarea', value: 'TextArea' },
|
||||
{ label: 'Phone', value: 'Phone' },
|
||||
{ label: 'URL', value: 'Url' },
|
||||
{ label: 'Number', value: 'Number' },
|
||||
{ label: 'Select Field', value: 'Dropdown' },
|
||||
{ label: 'Switch Toggle', value: 'Switch' },
|
||||
{ label: 'Date', value: 'Date' },
|
||||
{ label: 'Time', value: 'Time' },
|
||||
{ label: 'Date & Time', value: 'DateTime' },
|
||||
])
|
||||
|
||||
let selectedType = ref(dataTypes[0])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CustomFieldModal'
|
||||
})
|
||||
|
||||
const isSwitchTypeSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Switch Toggle'
|
||||
)
|
||||
|
||||
const isDropdownSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Select Field'
|
||||
)
|
||||
|
||||
const defaultValueComponent = computed(() => {
|
||||
if (customFieldStore.currentCustomField.type) {
|
||||
return defineAsyncComponent(() =>
|
||||
import(
|
||||
`../../custom-fields/types/${customFieldStore.currentCustomField.type}Type.vue`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const isRequiredField = computed({
|
||||
get: () => customFieldStore.currentCustomField.is_required === 1,
|
||||
set: (value) => {
|
||||
const intVal = value ? 1 : 0
|
||||
customFieldStore.currentCustomField.is_required = intVal
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCustomField: {
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
label: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
model_type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
order: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customFieldStore)
|
||||
)
|
||||
|
||||
function setData() {
|
||||
if (customFieldStore.isEdit) {
|
||||
selectedType.value = dataTypes.find(
|
||||
(type) => type.value == customFieldStore.currentCustomField.type
|
||||
)
|
||||
} else {
|
||||
customFieldStore.currentCustomField.model_type = modelTypes[0]
|
||||
|
||||
customFieldStore.currentCustomField.type = dataTypes[0].value
|
||||
selectedType.value = dataTypes[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomFieldData() {
|
||||
v$.value.currentCustomField.$touch()
|
||||
|
||||
if (v$.value.currentCustomField.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
...customFieldStore.currentCustomField,
|
||||
}
|
||||
|
||||
if (customFieldStore.currentCustomField.options) {
|
||||
data.options = customFieldStore.currentCustomField.options.map(
|
||||
(option) => option.name
|
||||
)
|
||||
}
|
||||
|
||||
if (data.type == 'Time' && typeof data.default_answer == 'object') {
|
||||
let HH =
|
||||
data && data.default_answer && data.default_answer.HH
|
||||
? data.default_answer.HH
|
||||
: null
|
||||
let mm =
|
||||
data && data.default_answer && data.default_answer.mm
|
||||
? data.default_answer.mm
|
||||
: null
|
||||
let ss =
|
||||
data && data.default_answer && data.default_answer.ss
|
||||
? data.default_answer.ss
|
||||
: null
|
||||
|
||||
data.default_answer = `${HH}:${mm}`
|
||||
}
|
||||
|
||||
const action = customFieldStore.isEdit
|
||||
? customFieldStore.updateCustomField
|
||||
: customFieldStore.addCustomField
|
||||
|
||||
await action(data)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCustomFieldModal()
|
||||
}
|
||||
|
||||
function addNewOption(option) {
|
||||
customFieldStore.currentCustomField.options = [
|
||||
{ name: option },
|
||||
...customFieldStore.currentCustomField.options,
|
||||
]
|
||||
}
|
||||
|
||||
function removeOption(index) {
|
||||
if (customFieldStore.isEdit && customFieldStore.currentCustomField.in_use) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = customFieldStore.currentCustomField.options[index]
|
||||
|
||||
if (option.name === customFieldStore.currentCustomField.default_answer) {
|
||||
customFieldStore.currentCustomField.default_answer = null
|
||||
}
|
||||
|
||||
customFieldStore.currentCustomField.options.splice(index, 1)
|
||||
}
|
||||
|
||||
function onChangeReset() {
|
||||
customFieldStore.$patch((state) => {
|
||||
state.currentCustomField.default_answer = null
|
||||
state.currentCustomField.is_required = false
|
||||
state.currentCustomField.placeholder = null
|
||||
state.currentCustomField.options = []
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function onSelectedTypeChange(data) {
|
||||
customFieldStore.currentCustomField.type = data.value
|
||||
}
|
||||
|
||||
function closeCustomFieldModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
customFieldStore.resetCurrentCustomField()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex items-center mt-1">
|
||||
<BaseInput
|
||||
v-model="option"
|
||||
type="text"
|
||||
class="w-full md:w-96"
|
||||
:placeholder="$t('settings.custom_fields.press_enter_to_add')"
|
||||
@click="onAddOption"
|
||||
@keydown.enter.prevent.stop="onAddOption"
|
||||
/>
|
||||
|
||||
<BaseIcon
|
||||
name="PlusCircleIcon"
|
||||
class="ml-1 text-primary-500 cursor-pointer"
|
||||
@click="onAddOption"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['onAdd'])
|
||||
|
||||
const option = ref(null)
|
||||
|
||||
function onAddOption() {
|
||||
if (option.value == null || option.value == '' || option.value == undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
emit('onAdd', option.value)
|
||||
|
||||
option.value = null
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.name.$error &&
|
||||
v$.doSpaceDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.doSpaceDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.doSpaceDiskConfig.name.$error"
|
||||
@input="v$.doSpaceDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.selected_driver.$error &&
|
||||
v$.doSpaceDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.doSpaceDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_root')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.root.$error &&
|
||||
v$.doSpaceDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.doSpaceDiskConfig.root.$error"
|
||||
@input="v$.doSpaceDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_key')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.key.$error &&
|
||||
v$.doSpaceDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.doSpaceDiskConfig.key.$error"
|
||||
@input="v$.doSpaceDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_secret')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.secret.$error &&
|
||||
v$.doSpaceDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.doSpaceDiskConfig.secret.$error"
|
||||
@input="v$.doSpaceDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_region')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.region.$error &&
|
||||
v$.doSpaceDiskConfig.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. nyc3"
|
||||
:invalid="v$.doSpaceDiskConfig.region.$error"
|
||||
@input="v$.doSpaceDiskConfig.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_endpoint')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.endpoint.$error &&
|
||||
v$.doSpaceDiskConfig.endpoint.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.endpoint"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. https://nyc3.digitaloceanspaces.com"
|
||||
:invalid="v$.doSpaceDiskConfig.endpoint.$error"
|
||||
@input="v$.doSpaceDiskConfig.endpoint.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_bucket')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.bucket.$error &&
|
||||
v$.doSpaceDiskConfig.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. my-new-space"
|
||||
:invalid="v$.doSpaceDiskConfig.bucket.$error"
|
||||
@input="v$.doSpaceDiskConfig.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, url, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.doSpaceDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
doSpaceDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
endpoint: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.doSpaceDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'doSpaces',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
endpoint: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'doSpaces',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(
|
||||
diskStore.doSpaceDiskConfig,
|
||||
JSON.parse(modalStore.data.credentials)
|
||||
)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.doSpaceDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'doSpaces')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.doSpaceDiskConfig.$touch()
|
||||
if (v$.value.doSpaceDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.doSpaceDiskConfig,
|
||||
name: diskStore.doSpaceDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.doSpaceDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
selected_disk,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.name.$error &&
|
||||
v$.dropBoxDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.dropBoxDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.name.$error"
|
||||
@input="v$.dropBoxDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.driver')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.selected_driver.$error &&
|
||||
v$.dropBoxDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.dropBoxDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_root')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.root.$error &&
|
||||
v$.dropBoxDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.dropBoxDiskConfig.root.$error"
|
||||
@input="v$.dropBoxDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_token')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.token.$error &&
|
||||
v$.dropBoxDiskConfig.token.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.token"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.token.$error"
|
||||
@input="v$.dropBoxDiskConfig.token.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_key')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.key.$error &&
|
||||
v$.dropBoxDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.dropBoxDiskConfig.key.$error"
|
||||
@input="v$.dropBoxDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_secret')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.secret.$error &&
|
||||
v$.dropBoxDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.dropBoxDiskConfig.secret.$error"
|
||||
@input="v$.dropBoxDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_app')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.app.$error &&
|
||||
v$.dropBoxDiskConfig.app.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.app"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.app.$error"
|
||||
@input="v$.dropBoxDiskConfig.app.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { reactive, ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let is_current_disk = ref(null)
|
||||
let selected_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.dropBoxDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
dropBoxDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
token: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
app: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.dropBoxDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'dropbox',
|
||||
token: null,
|
||||
key: null,
|
||||
secret: null,
|
||||
app: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'dropbox',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.dropBoxDiskConfig, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.dropBoxDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'dropbox')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.dropBoxDiskConfig.$touch()
|
||||
if (v$.value.dropBoxDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
credentials: diskStore.dropBoxDiskConfig,
|
||||
name: diskStore.dropBoxDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.dropBoxDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
is_current_disk,
|
||||
selected_disk,
|
||||
isDisabled,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="submitData">
|
||||
<div class="px-4 sm:px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.localDiskConfig.name.$error &&
|
||||
v$.localDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.localDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.name.$error"
|
||||
@input="v$.localDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.localDiskConfig.selected_driver.$error &&
|
||||
v$.localDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
value-prop="value"
|
||||
:invalid="v$.localDiskConfig.selected_driver.$error"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.local_root')"
|
||||
:error="
|
||||
v$.localDiskConfig.root.$error &&
|
||||
v$.localDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.localDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.root.$error"
|
||||
placeholder="Ex./user/root/"
|
||||
@input="v$.localDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.localDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
localDiskConfig: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.localDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'local',
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
|
||||
let data = reactive({
|
||||
disk: 'local',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.localDiskConfig, modalStore.data)
|
||||
diskStore.localDiskConfig.root = modalStore.data.credentials
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.localDiskConfig, diskData.data)
|
||||
}
|
||||
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'local')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.localDiskConfig.$touch()
|
||||
|
||||
if (v$.value.localDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = reactive({
|
||||
credentials: diskStore.localDiskConfig.root,
|
||||
name: diskStore.localDiskConfig.name,
|
||||
driver: diskStore.localDiskConfig.selected_driver,
|
||||
set_as_default: set_as_default.value,
|
||||
})
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.localDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
selected_driver,
|
||||
selected_disk,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
is_current_disk,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.name.$error &&
|
||||
v$.s3DiskConfigData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.s3DiskConfigData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.s3DiskConfigData.name.$error"
|
||||
@input="v$.s3DiskConfigData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.selected_driver.$error &&
|
||||
v$.s3DiskConfigData.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.s3DiskConfigData.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_root')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.root.$error &&
|
||||
v$.s3DiskConfigData.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.s3DiskConfigData.root.$error"
|
||||
@input="v$.s3DiskConfigData.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_key')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.key.$error &&
|
||||
v$.s3DiskConfigData.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.s3DiskConfigData.key.$error"
|
||||
@input="v$.s3DiskConfigData.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_secret')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.secret.$error &&
|
||||
v$.s3DiskConfigData.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.s3DiskConfigData.secret.$error"
|
||||
@input="v$.s3DiskConfigData.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_region')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.region.$error &&
|
||||
v$.s3DiskConfigData.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. us-west"
|
||||
:invalid="v$.s3DiskConfigData.region.$error"
|
||||
@input="v$.s3DiskConfigData.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_bucket')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.bucket.$error &&
|
||||
v$.s3DiskConfigData.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. AppName"
|
||||
:invalid="v$.s3DiskConfigData.bucket.$error"
|
||||
@input="v$.s3DiskConfigData.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let selected_disk = ref(null)
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.s3DiskConfigData.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
s3DiskConfigData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.s3DiskConfigData = {
|
||||
name: null,
|
||||
selected_driver: 's3',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 's3',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.s3DiskConfigData, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.s3DiskConfigData, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 's3')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.s3DiskConfigData.$touch()
|
||||
if (v$.value.s3DiskConfigData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.s3DiskConfigData,
|
||||
name: diskStore.s3DiskConfigData.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.s3DiskConfigData.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
selected_disk,
|
||||
selected_driver,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user