v6 update

This commit is contained in:
Mohit Panjwani
2022-01-10 16:06:17 +05:30
parent b770e6277f
commit bdea879273
722 changed files with 19047 additions and 9186 deletions

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>