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,110 @@
<template>
<div class="flex flex-col items-center justify-between w-full pt-10">
<img
id="logo-crater"
src="/img/crater-logo.png"
alt="Crater Logo"
class="h-12 mb-5 md:mb-10"
/>
<BaseWizard
:steps="7"
:current-step="currentStepNumber"
@click="onNavClick"
>
<component :is="stepComponent" @next="onStepChange" />
</BaseWizard>
</div>
</template>
<script>
import { ref } from 'vue'
import Step1RequirementsCheck from './Step1RequirementsCheck.vue'
import Step2PermissionCheck from './Step2PermissionCheck.vue'
import Step3DatabaseConfig from './Step3DatabaseConfig.vue'
import Step4VerifyDomain from './Step4VerifyDomain.vue'
import Step5EmailConfig from './Step5EmailConfig.vue'
import Step6AccountSettings from './Step6AccountSettings.vue'
import Step7CompanyInfo from './Step7CompanyInfo.vue'
import Step8CompanyPreferences from './Step8CompanyPreferences.vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { useRouter } from 'vue-router'
export default {
components: {
step_1: Step1RequirementsCheck,
step_2: Step2PermissionCheck,
step_3: Step3DatabaseConfig,
step_4: Step4VerifyDomain,
step_5: Step5EmailConfig,
step_6: Step6AccountSettings,
step_7: Step7CompanyInfo,
step_8: Step8CompanyPreferences,
},
setup() {
let stepComponent = ref('step_1')
let currentStepNumber = ref(1)
const router = useRouter()
const installationStore = useInstallationStore()
checkCurrentProgress()
async function checkCurrentProgress() {
let res = await installationStore.fetchInstallationStep()
if (res.data.profile_complete === 'COMPLETED') {
router.push('/admin/dashboard')
return
}
let dbstep = parseInt(res.data.profile_complete)
if (dbstep) {
currentStepNumber.value = dbstep + 1
stepComponent.value = `step_${dbstep + 1}`
}
}
async function saveStepProgress(data) {
let status = {
profile_complete: data,
}
try {
await installationStore.addInstallationStep(status)
return true
} catch (e) {
if (e?.response?.data?.message === 'The MAC is invalid.') {
window.location.reload()
}
return false
}
}
async function onStepChange(data) {
if (data) {
let res = await saveStepProgress(data)
if (!res) return false
}
currentStepNumber.value++
if (currentStepNumber.value <= 8) {
stepComponent.value = 'step_' + currentStepNumber.value
}
}
function onNavClick(e) {}
return {
stepComponent,
currentStepNumber,
onStepChange,
saveStepProgress,
onNavClick,
}
},
}
</script>

View File

@ -0,0 +1,114 @@
<template>
<BaseWizardStep
:title="$t('wizard.req.system_req')"
:description="$t('wizard.req.system_req_desc')"
>
<div class="w-full md:w-2/3">
<div class="mb-6">
<div
v-if="phpSupportInfo"
class="grid grid-flow-row grid-cols-3 p-3 border border-gray-200 lg:gap-24 sm:gap-4"
>
<div class="col-span-2 text-sm">
{{
$t('wizard.req.php_req_version', {
version: phpSupportInfo.minimum,
})
}}
</div>
<div class="text-right">
{{ phpSupportInfo.current }}
<span
v-if="phpSupportInfo.supported"
class="inline-block w-4 h-4 ml-3 mr-2 bg-green-500 rounded-full"
/>
<span
v-else
class="inline-block w-4 h-4 ml-3 mr-2 bg-red-500 rounded-full"
/>
</div>
</div>
<div v-if="requirements">
<div
v-for="(requirement, index) in requirements"
:key="index"
class="grid grid-flow-row grid-cols-3 p-3 border border-gray-200 lg:gap-24 sm:gap-4"
>
<div class="col-span-2 text-sm">
{{ index }}
</div>
<div class="text-right">
<span
v-if="requirement"
class="inline-block w-4 h-4 ml-3 mr-2 bg-green-500 rounded-full"
/>
<span
v-else
class="inline-block w-4 h-4 ml-3 mr-2 bg-red-500 rounded-full"
/>
</div>
</div>
</div>
</div>
<BaseButton v-if="hasNext" @click="next">
{{ $t('wizard.continue') }}
<template #left="slotProps">
<BaseIcon name="ArrowRightIcon" :class="slotProps.class" />
</template>
</BaseButton>
<BaseButton
v-if="!requirements"
:loading="isSaving"
:disabled="isSaving"
@click="getRequirements"
>
{{ $t('wizard.req.check_req') }}
</BaseButton>
</div>
</BaseWizardStep>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation.js'
const emit = defineEmits(['next'])
const requirements = ref('')
const phpSupportInfo = ref('')
const isSaving = ref(false)
const isShow = ref(true)
const installationStore = useInstallationStore()
const hasNext = computed(() => {
if (requirements.value) {
let isRequired = true
for (const key in requirements.value) {
if (!requirements.value[key]) {
isRequired = false
}
return requirements.value && phpSupportInfo.value.supported && isRequired
}
}
return false
})
async function getRequirements() {
isSaving.value = true
const response = await installationStore.fetchInstallationRequirements()
if (response.data) {
requirements.value = response?.data?.requirements?.requirements?.php
phpSupportInfo.value = response?.data?.phpSupportInfo
}
}
function next() {
isSaving.value = true
emit('next')
isSaving.value = false
}
</script>

View File

@ -0,0 +1,123 @@
<template>
<BaseWizardStep
:title="$t('wizard.permissions.permissions')"
:description="$t('wizard.permissions.permission_desc')"
>
<!-- Content Placeholders -->
<BaseContentPlaceholders v-if="isFetchingInitialData">
<div
v-for="(permission, index) in 3"
:key="index"
class="
grid grid-flow-row grid-cols-3
lg:gap-24
sm:gap-4
border border-gray-200
"
>
<BaseContentPlaceholdersText :lines="1" class="col-span-4 p-3" />
</div>
<BaseContentPlaceholdersBox
:rounded="true"
class="mt-10"
style="width: 96px; height: 42px"
/>
</BaseContentPlaceholders>
<!-- End of Content Placeholder -->
<div v-else class="relative">
<div
v-for="(permission, index) in permissions"
:key="index"
class="border border-gray-200"
>
<div class="grid grid-flow-row grid-cols-3 lg:gap-24 sm:gap-4">
<div class="col-span-2 p-3">
{{ permission.folder }}
</div>
<div class="p-3 text-right">
<span
v-if="permission.isSet"
class="inline-block w-4 h-4 ml-3 mr-2 rounded-full bg-green-500"
/>
<span
v-else
class="inline-block w-4 h-4 ml-3 mr-2 rounded-full bg-red-500"
/>
<span>{{ permission.permission }}</span>
</div>
</div>
</div>
<BaseButton
v-show="!isFetchingInitialData"
class="mt-10"
:loading="isSaving"
:disabled="isSaving"
@click="next"
>
<template #left="slotProps">
<BaseIcon name="ArrowRightIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.continue') }}
</BaseButton>
</div>
</BaseWizardStep>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { useDialogStore } from '@/scripts/stores/dialog'
import { useI18n } from 'vue-i18n'
const emit = defineEmits(['next'])
let isFetchingInitialData = ref(false)
let isSaving = ref(false)
let permissions = ref([])
const { tm, t } = useI18n()
const installationStore = useInstallationStore()
const dialogStore = useDialogStore()
onMounted(() => {
getPermissions()
})
async function getPermissions() {
isFetchingInitialData.value = true
const res = await installationStore.fetchInstallationPermissions()
permissions.value = res.data.permissions.permissions
if (res.data && res.data.permissions.errors) {
setTimeout(() => {
dialogStore
.openDialog({
title: tm('wizard.permissions.permission_confirm_title'),
message: t('wizard.permissions.permission_confirm_desc'),
yesLabel: 'OK',
noLabel: 'Cancel',
variant: 'danger',
hideNoButton: false,
size: 'lg',
})
.then((res) => {
if (res.data) {
isFetchingInitialData.value = false
}
})
}, 500)
}
isFetchingInitialData.value = false
}
function next() {
isSaving.value = true
emit('next')
isSaving.value = false
}
</script>

View File

@ -0,0 +1,131 @@
<template>
<BaseWizardStep
:title="$t('wizard.database.database')"
:description="$t('wizard.database.desc')"
step-container="w-full p-8 mb-8 bg-white border border-gray-200 border-solid rounded md:w-full"
>
<component
:is="databaseData.database_connection"
:config-data="databaseData"
:is-saving="isSaving"
@on-change-driver="getDatabaseConfig"
@submit-data="next"
/>
</BaseWizardStep>
</template>
<script>
import { ref, computed } from 'vue'
import Mysql from './database/MysqlDatabase.vue'
import Pgsql from './database/PgsqlDatabase.vue'
import Sqlite from './database/SqliteDatabase.vue'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { useI18n } from 'vue-i18n'
export default {
components: {
Mysql,
Pgsql,
Sqlite,
},
emits: ['next'],
setup(props, { emit }) {
const database_connection = ref('mysql')
const isSaving = ref(false)
const { t } = useI18n()
const notificationStore = useNotificationStore()
const installationStore = useInstallationStore()
const databaseData = computed(() => {
return installationStore.currentDataBaseData
})
async function getDatabaseConfig(connection) {
let params = {
connection,
}
const res = await installationStore.fetchInstallationDatabase(params)
if (res.data.success) {
databaseData.value.database_connection =
res.data.config.database_connection
}
if (connection === 'sqlite') {
databaseData.value.database_name = res.data.config.database_name
} else {
databaseData.value.database_name = null
}
}
async function next(databaseData) {
isSaving.value = true
try {
let res = await installationStore.addInstallationDatabase(databaseData)
isSaving.value = false
if (res.data.success) {
await installationStore.addInstallationFinish()
emit('next', 3)
notificationStore.showNotification({
type: 'success',
message: t('wizard.success.' + res.data.success),
})
return
} else if (res.data.error) {
if (res.data.requirement) {
notificationStore.showNotification({
type: 'error',
message: t('wizard.errors.' + res.data.error, {
version: res.data.requirement.minimum,
name: databaseData.value.database_connection,
}),
})
return
}
notificationStore.showNotification({
type: 'error',
message: t('wizard.errors.' + res.data.error),
})
} else if (res.data.errors) {
notificationStore.showNotification({
type: 'error',
message: res.data.errors[0],
})
} else if (res.data.error_message) {
notificationStore.showNotification({
type: 'error',
message: res.data.error_message,
})
}
} catch (e) {
notificationStore.showNotification({
type: 'error',
message: t('validation.something_went_wrong'),
})
isSaving.value = false
} finally {
isSaving.value = false
}
}
return {
databaseData,
database_connection,
isSaving,
getDatabaseConfig,
next,
}
},
}
</script>

View File

@ -0,0 +1,108 @@
<template>
<BaseWizardStep
:title="$t('wizard.verify_domain.title')"
:description="$t('wizard.verify_domain.desc')"
>
<div class="w-full md:w-2/3">
<BaseInputGroup
:label="$t('wizard.verify_domain.app_domain')"
:error="v$.app_domain.$error && v$.app_domain.$errors[0].$message"
required
>
<BaseInput
v-model="formData.app_domain"
:invalid="v$.app_domain.$error"
type="text"
@input="v$.app_domain.$touch()"
/>
</BaseInputGroup>
</div>
<p class="mt-4 mb-0 text-sm text-gray-600">Notes:</p>
<ul class="w-full text-gray-600 list-disc list-inside">
<li class="text-sm leading-8">
App domain should not contain
<b class="inline-block px-1 bg-gray-100 rounded-sm">https://</b> or
<b class="inline-block px-1 bg-gray-100 rounded-sm">http</b> in front of
the domain.
</li>
<li class="text-sm leading-8">
If you're accessing the website on a different port, please mention the
port. For example:
<b class="inline-block px-1 bg-gray-100">localhost:8080</b>
</li>
</ul>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
class="mt-8"
@click="verifyDomain"
>
{{ $t('wizard.verify_domain.verify_now') }}
</BaseButton>
</BaseWizardStep>
</template>
<script setup>
import { required, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { ref, inject, computed, reactive } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useI18n } from 'vue-i18n'
const emit = defineEmits(['next'])
const formData = reactive({
app_domain: window.location.origin.replace(/(^\w+:|^)\/\//, ''),
})
const isSaving = ref(false)
const { t } = useI18n()
const utils = inject('utils')
const isUrl = (value) => utils.checkValidDomainUrl(value)
const installationStore = useInstallationStore()
const notificationStore = useNotificationStore()
const rules = {
app_domain: {
required: helpers.withMessage(t('validation.required'), required),
isUrl: helpers.withMessage(t('validation.invalid_domain_url'), isUrl),
},
}
const v$ = useVuelidate(
rules,
computed(() => formData)
)
async function verifyDomain() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
isSaving.value = true
try {
await installationStore.setInstallationDomain(formData)
await installationStore.installationLogin()
let driverRes = await installationStore.checkAutheticated()
if (driverRes.data) {
emit('next', 4)
}
isSaving.value = false
} catch (e) {
notificationStore.showNotification({
type: 'error',
message: t('wizard.verify_domain.failed'),
})
isSaving.value = false
}
}
</script>

View File

@ -0,0 +1,77 @@
<template>
<BaseWizardStep
:title="$t('wizard.mail.mail_config')"
:description="$t('wizard.mail.mail_config_desc')"
>
<form action="" @submit.prevent="next">
<component
:is="mailDriverStore.mail_driver"
:config-data="mailDriverStore.mailConfigData"
:is-saving="isSaving"
:is-fetching-initial-data="isFetchingInitialData"
@on-change-driver="(val) => changeDriver(val)"
@submit-data="next"
/>
</form>
</BaseWizardStep>
</template>
<script>
import Smtp from './mail-driver/SmtpMailDriver.vue'
import Mailgun from './mail-driver/MailgunMailDriver.vue'
import Ses from './mail-driver/SesMailDriver.vue'
import Basic from './mail-driver/BasicMailDriver.vue'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
import { ref } from 'vue'
export default {
components: {
Smtp,
Mailgun,
Ses,
sendmail: Basic,
Mail: Basic,
},
emits: ['next'],
setup(props, { emit }) {
const isSaving = ref(false)
const isFetchingInitialData = ref(false)
const mailDriverStore = useMailDriverStore()
mailDriverStore.mail_driver = 'mail'
loadData()
function changeDriver(value) {
mailDriverStore.mail_driver = value
}
async function loadData() {
isFetchingInitialData.value = true
await mailDriverStore.fetchMailDrivers()
isFetchingInitialData.value = false
}
async function next(mailConfigData) {
isSaving.value = true
let res = await mailDriverStore.updateMailConfig(mailConfigData)
isSaving.value = false
if (res.data.success) {
await emit('next', 5)
}
}
return {
mailDriverStore,
isSaving,
isFetchingInitialData,
changeDriver,
next,
}
},
}
</script>

View File

@ -0,0 +1,229 @@
<template>
<BaseWizardStep
:title="$t('wizard.account_info')"
:description="$t('wizard.account_info_desc')"
>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$tc('settings.account_settings.profile_picture')"
>
<BaseFileUploader
:avatar="true"
:preview-image="avatarUrl"
@change="onFileInputChange"
@remove="onFileInputRemove"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.name')"
:error="
v$.userForm.name.$error && v$.userForm.name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="userForm.name"
:invalid="v$.userForm.name.$error"
type="text"
name="name"
@input="v$.userForm.name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.email')"
:error="
v$.userForm.email.$error && v$.userForm.email.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="userForm.email"
:invalid="v$.userForm.email.$error"
type="text"
name="email"
@input="v$.userForm.email.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.password')"
:error="
v$.userForm.password.$error &&
v$.userForm.password.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="userForm.password"
:invalid="v$.userForm.password.$error"
:type="isShowPassword ? 'text' : 'password'"
name="password"
@input="v$.userForm.password.$touch()"
>
<template #right>
<EyeOffIcon
v-if="isShowPassword"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<EyeIcon
v-else
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.confirm_password')"
:error="
v$.userForm.confirm_password.$error &&
v$.userForm.confirm_password.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="userForm.confirm_password"
:invalid="v$.userForm.confirm_password.$error"
:type="isShowConfirmPassword ? 'text' : 'password'"
name="confirm_password"
@input="v$.userForm.confirm_password.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowConfirmPassword"
name="EyeOffIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowConfirmPassword = !isShowConfirmPassword"
/>
<BaseIcon
v-else
name="EyeIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowConfirmPassword = !isShowConfirmPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
</div>
<BaseButton :loading="isSaving" :disabled="isSaving" class="mt-4">
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</form>
</BaseWizardStep>
</template>
<script setup>
import {
helpers,
required,
requiredIf,
sameAs,
minLength,
email,
} from '@vuelidate/validators'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useVuelidate } from '@vuelidate/core'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useCompanyStore } from '@/scripts/admin/stores/company'
const emit = defineEmits(['next'])
let isSaving = ref(false)
const isShowPassword = ref(false)
const isShowConfirmPassword = ref(false)
let avatarUrl = ref('')
let avatarFileBlob = ref(null)
const userStore = useUserStore()
const companyStore = useCompanyStore()
const { t } = useI18n()
const userForm = computed(() => {
return userStore.userForm
})
const rules = computed(() => {
return {
userForm: {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
email: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
password: {
required: helpers.withMessage(t('validation.required'), required),
minLength: helpers.withMessage(
t('validation.password_min_length', { count: 8 }),
minLength(8)
),
},
confirm_password: {
required: helpers.withMessage(
t('validation.required'),
requiredIf(userStore.userForm.password)
),
sameAsPassword: helpers.withMessage(
t('validation.password_incorrect'),
sameAs(userStore.userForm.password)
),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => userStore)
)
function onFileInputChange(fileName, file) {
avatarFileBlob.value = file
}
function onFileInputRemove() {
avatarFileBlob.value = null
}
async function next() {
v$.value.userForm.$touch()
if (v$.value.userForm.$invalid) {
return true
}
isSaving.value = true
let res = await userStore.updateCurrentUser(userForm.value)
isSaving.value = false
if (res.data.data) {
if (avatarFileBlob.value) {
let avatarData = new FormData()
avatarData.append('admin_avatar', avatarFileBlob.value)
await userStore.uploadAvatar(avatarData)
}
const company = res.data.data.companies[0]
await companyStore.setSelectedCompany(company)
emit('next', 6)
}
}
</script>

View File

@ -0,0 +1,247 @@
<template>
<BaseWizardStep
:title="$t('wizard.company_info')"
:description="$t('wizard.company_info_desc')"
step-container="bg-white border border-gray-200 border-solid mb-8 md:w-full p-8 rounded w-full"
>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup :label="$tc('settings.company_info.company_logo')">
<BaseFileUploader
base64
:preview-image="previewLogo"
@change="onFileInputChange"
@remove="onFileInputRemove"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.company_name')"
:error="
v$.companyForm.name.$error &&
v$.companyForm.name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="companyForm.name"
:invalid="v$.companyForm.name.$error"
type="text"
name="name"
@input="v$.companyForm.name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.country')"
:error="
v$.companyForm.address.country_id.$error &&
v$.companyForm.address.country_id.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="companyForm.address.country_id"
label="name"
:invalid="v$.companyForm.address.country_id.$error"
:options="globalStore.countries"
value-prop="id"
:can-deselect="false"
:can-clear="false"
:content-loading="isFetchingInitialData"
:placeholder="$t('general.select_country')"
searchable
track-by="name"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup :label="$t('wizard.state')">
<BaseInput
v-model="companyForm.address.state"
name="state"
type="text"
/>
</BaseInputGroup>
<BaseInputGroup :label="$t('wizard.city')">
<BaseInput
v-model="companyForm.address.city"
name="city"
type="text"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<div>
<BaseInputGroup
:label="$t('wizard.address')"
:error="
v$.companyForm.address.address_street_1.$error &&
v$.companyForm.address.address_street_1.$errors[0].$message
"
>
<BaseTextarea
v-model.trim="companyForm.address.address_street_1"
:invalid="v$.companyForm.address.address_street_1.$error"
:placeholder="$t('general.street_1')"
name="billing_street1"
rows="2"
@input="v$.companyForm.address.address_street_1.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:error="
v$.companyForm.address.address_street_2.$error &&
v$.companyForm.address.address_street_2.$errors[0].$message
"
class="mt-1 lg:mt-2 md:mt-2"
>
<BaseTextarea
v-model="companyForm.address.address_street_2"
:invalid="v$.companyForm.address.address_street_2.$error"
:placeholder="$t('general.street_2')"
name="billing_street2"
rows="2"
@input="v$.companyForm.address.address_street_2.$touch()"
/>
</BaseInputGroup>
</div>
<div>
<BaseInputGroup :label="$t('wizard.zip_code')">
<BaseInput
v-model.trim="companyForm.address.zip"
type="text"
name="zip"
/>
</BaseInputGroup>
<BaseInputGroup :label="$t('wizard.phone')" class="mt-4">
<BaseInput
v-model.trim="companyForm.address.phone"
type="text"
name="phone"
/>
</BaseInputGroup>
</div>
</div>
<BaseButton :loading="isSaving" :disabled="isSaving" class="mt-4">
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</form>
</BaseWizardStep>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { required, maxLength, helpers } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { useCompanyStore } from '@/scripts/admin/stores/company'
const emit = defineEmits(['next'])
let isFetchingInitialData = ref(false)
let isSaving = ref(false)
const { t } = useI18n()
let previewLogo = ref(null)
let logoFileBlob = ref(null)
let logoFileName = ref(null)
const companyForm = reactive({
name: null,
address: {
address_street_1: '',
address_street_2: '',
website: '',
country_id: null,
state: '',
city: '',
phone: '',
zip: '',
},
})
const companyStore = useCompanyStore()
const globalStore = useGlobalStore()
onMounted(async () => {
isFetchingInitialData.value = true
await globalStore.fetchCountries()
isFetchingInitialData.value = false
})
const rules = {
companyForm: {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
address: {
country_id: {
required: helpers.withMessage(t('validation.required'), required),
},
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, { companyForm })
function onFileInputChange(fileName, file, fileCount, fileList) {
logoFileName.value = fileList.name
logoFileBlob.value = file
}
function onFileInputRemove() {
logoFileBlob.value = null
}
async function next() {
v$.value.companyForm.$touch()
if (v$.value.$invalid) {
return true
}
isSaving.value = true
let res = companyStore.updateCompany(companyForm)
if (res) {
if (logoFileBlob.value) {
let logoData = new FormData()
logoData.append(
'company_logo',
JSON.stringify({
name: logoFileName.value,
data: logoFileBlob.value,
})
)
await companyStore.updateCompanyLogo(logoData)
}
isSaving.value = false
emit('next', 7)
}
}
</script>

View File

@ -0,0 +1,309 @@
<template>
<BaseWizardStep
:title="$t('wizard.preferences')"
:description="$t('wizard.preferences_desc')"
step-container="bg-white border border-gray-200 border-solid mb-8 md:w-full p-8 rounded w-full"
>
<form action="" @submit.prevent="next">
<div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.currency')"
:error="
v$.currentPreferences.currency.$error &&
v$.currentPreferences.currency.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="currentPreferences.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$.currentPreferences.currency.$error"
class="w-full"
>
</BaseMultiselect>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.preferences.default_language')"
:error="
v$.currentPreferences.language.$error &&
v$.currentPreferences.language.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="currentPreferences.language"
:content-loading="isFetchingInitialData"
:options="globalStore.languages"
label="name"
value-prop="code"
:placeholder="$tc('settings.preferences.select_language')"
class="w-full"
track-by="code"
:searchable="true"
:invalid="v$.currentPreferences.language.$error"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.date_format')"
:error="
v$.currentPreferences.carbon_date_format.$error &&
v$.currentPreferences.carbon_date_format.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="currentPreferences.carbon_date_format"
:content-loading="isFetchingInitialData"
:options="globalStore.dateFormats"
label="display_date"
value-prop="carbon_format_value"
:placeholder="$tc('settings.preferences.select_date_format')"
track-by="display_date"
searchable
:invalid="v$.currentPreferences.carbon_date_format.$error"
class="w-full"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.time_zone')"
:error="
v$.currentPreferences.time_zone.$error &&
v$.currentPreferences.time_zone.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="currentPreferences.time_zone"
:content-loading="isFetchingInitialData"
:options="globalStore.timeZones"
label="key"
value-prop="value"
:placeholder="$tc('settings.preferences.select_time_zone')"
track-by="value"
:searchable="true"
:invalid="v$.currentPreferences.time_zone.$error"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.fiscal_year')"
:error="
v$.currentPreferences.fiscal_year.$error &&
v$.currentPreferences.fiscal_year.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model="currentPreferences.fiscal_year"
:content-loading="isFetchingInitialData"
:options="globalStore.fiscalYears"
label="key"
value-prop="value"
:placeholder="$tc('settings.preferences.select_financial_year')"
:invalid="v$.currentPreferences.fiscal_year.$error"
track-by="key"
:searchable="true"
class="w-full"
/>
</BaseInputGroup>
</div>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
:content-loading="isFetchingInitialData"
class="mt-4"
>
<template #left="slotProps">
<BaseIcon name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</div>
</form>
</BaseWizardStep>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { ref, computed, onMounted, reactive } from 'vue'
import { required, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import Ls from '@/scripts/services/ls.js'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useDialogStore } from '@/scripts/stores/dialog'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useRouter } from 'vue-router'
const emit = defineEmits(['next'])
const isSaving = ref(false)
let isFetchingInitialData = ref(false)
let currentPreferences = reactive({
currency: 1,
language: 'en',
carbon_date_format: 'd M Y',
time_zone: 'UTC',
fiscal_year: '1-12',
})
const { tm, t } = useI18n()
const router = useRouter()
isFetchingInitialData.value = true
const options = reactive([
{
title: tm('settings.customization.invoices.allow'),
value: 'allow',
},
{
title: tm(
'settings.customization.invoices.disable_on_invoice_partial_paid'
),
value: 'disable_on_invoice_partial_paid',
},
{
title: tm('settings.customization.invoices.disable_on_invoice_paid'),
value: 'disable_on_invoice_paid',
},
{
title: tm('settings.customization.invoices.disable_on_invoice_sent'),
value: 'disable_on_invoice_sent',
},
])
const dialogStore = useDialogStore()
const globalStore = useGlobalStore()
const companyStore = useCompanyStore()
const userStore = useUserStore()
const notificationStore = useNotificationStore()
let fiscalYears = {
key: 'fiscal_years',
}
let data = {
key: 'languages',
}
isFetchingInitialData.value = true
Promise.all([
globalStore.fetchCurrencies(),
globalStore.fetchDateFormats(),
globalStore.fetchTimeZones(),
globalStore.fetchCountries(),
globalStore.fetchConfig(fiscalYears),
globalStore.fetchConfig(data),
]).then(([res1]) => {
isFetchingInitialData.value = false
})
const rules = computed(() => {
return {
currentPreferences: {
currency: {
required: helpers.withMessage(t('validation.required'), required),
},
language: {
required: helpers.withMessage(t('validation.required'), required),
},
carbon_date_format: {
required: helpers.withMessage(t('validation.required'), required),
},
time_zone: {
required: helpers.withMessage(t('validation.required'), required),
},
fiscal_year: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(rules, { currentPreferences })
async function next() {
v$.value.currentPreferences.$touch()
if (v$.value.$invalid) {
return true
}
dialogStore
.openDialog({
title: t('general.do_you_wish_to_continue'),
message: t('wizard.currency_set_alert'),
yesLabel: t('general.ok'),
noLabel: t('general.cancel'),
variant: 'danger',
size: 'lg',
hideNoButton: false,
})
.then(async (res) => {
if (res) {
let data = {
settings: {
...currentPreferences,
},
}
isSaving.value = true
delete data.settings.discount_per_item
let res = await companyStore.updateCompanySettings({
data,
})
if (res.data) {
isSaving.value = false
let data = {
settings: {
language: currentPreferences.language,
},
}
let res1 = await userStore.updateUserSettings(data)
if (res1.data) {
emit('next', 'COMPLETED')
notificationStore.showNotification({
type: 'success',
message: 'Login Successful',
})
router.push('/admin/dashboard')
}
Ls.set('auth.token', res.data.token)
}
return true
}
isSaving.value = false
return true
})
}
</script>

View File

@ -0,0 +1,188 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:error="v$.app_url.$error && v$.app_url.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.app_url"
:invalid="v$.app_url.$error"
type="text"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.connection')"
:error="
v$.database_connection.$error &&
v$.database_connection.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="databaseData.database_connection"
:invalid="v$.database_connection.$error"
:options="connections"
:can-deselect="false"
:can-clear="false"
@update:modelValue="onChangeConnection"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.port')"
:error="v$.database_port.$error && v$.database_port.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.database_port"
:invalid="v$.database_port.$error"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.db_name')"
:error="v$.database_name.$error && v$.database_name.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.database_name"
:invalid="v$.database_name.$error"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.username')"
:error="
v$.database_username.$error &&
v$.database_username.$errors[0].$message
"
required
>
<BaseInput
v-model="databaseData.database_username"
:invalid="v$.database_username.$error"
/>
</BaseInputGroup>
<BaseInputGroup :label="$t('wizard.database.password')">
<BaseInput v-model="databaseData.database_password" type="password" />
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.host')"
:error="
v$.database_hostname.$error &&
v$.database_hostname.$errors[0].$message
"
required
>
<BaseInput
v-model="databaseData.database_hostname"
:invalid="v$.database_hostname.$error"
/>
</BaseInputGroup>
</div>
<BaseButton
type="submit"
class="mt-4"
:loading="isSaving"
:disabled="isSaving"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</form>
</template>
<script setup>
import { computed, onMounted, reactive, inject } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { helpers, required, numeric } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const connections = reactive(['sqlite', 'mysql', 'pgsql'])
const { t } = useI18n()
const utils = inject('utils')
const installationStore = useInstallationStore()
onMounted(() => {
for (const key in databaseData.value) {
if (props.configData.hasOwnProperty(key)) {
databaseData.value[key] = props.configData[key]
}
}
})
const databaseData = computed(() => {
return installationStore.currentDataBaseData
})
const isUrl = (value) => utils.checkValidUrl(value)
const rules = {
database_connection: {
required: helpers.withMessage(t('validation.required'), required),
},
database_hostname: {
required: helpers.withMessage(t('validation.required'), required),
},
database_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric: numeric,
},
database_name: {
required: helpers.withMessage(t('validation.required'), required),
},
database_username: {
required: helpers.withMessage(t('validation.required'), required),
},
app_url: {
required: helpers.withMessage(t('validation.required'), required),
isUrl: helpers.withMessage(t('validation.invalid_url'), isUrl),
},
}
const v$ = useVuelidate(rules, databaseData.value)
function next() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
emit('submit-data', databaseData.value)
}
function onChangeConnection() {
v$.value.database_connection.$touch()
emit('on-change-driver', databaseData.value.database_connection)
}
</script>

View File

@ -0,0 +1,210 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:content-loading="isFetchingInitialData"
:error="v$.app_url.$error && v$.app_url.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.app_url"
:content-loading="isFetchingInitialData"
:invalid="v$.app_url.$error"
type="text"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.connection')"
:content-loading="isFetchingInitialData"
:error="
v$.database_connection.$error &&
v$.database_connection.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="databaseData.database_connection"
:content-loading="isFetchingInitialData"
:invalid="v$.database_connection.$error"
:options="connections"
:can-deselect="false"
:can-clear="false"
@update:modelValue="onChangeConnection"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.port')"
:content-loading="isFetchingInitialData"
:error="v$.database_port.$error && v$.database_port.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.database_port"
:content-loading="isFetchingInitialData"
:invalid="v$.database_port.$error"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.db_name')"
:content-loading="isFetchingInitialData"
:error="v$.database_name.$error && v$.database_name.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.database_name"
:content-loading="isFetchingInitialData"
:invalid="v$.database_name.$error"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.username')"
:content-loading="isFetchingInitialData"
:error="
v$.database_username.$error &&
v$.database_username.$errors[0].$message
"
required
>
<BaseInput
v-model="databaseData.database_username"
:content-loading="isFetchingInitialData"
:invalid="v$.database_username.$error"
/>
</BaseInputGroup>
<BaseInputGroup
:content-loading="isFetchingInitialData"
:label="$t('wizard.database.password')"
>
<BaseInput
v-model="databaseData.database_password"
:content-loading="isFetchingInitialData"
type="password"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.host')"
:content-loading="isFetchingInitialData"
:error="
v$.database_hostname.$error &&
v$.database_hostname.$errors[0].$message
"
required
>
<BaseInput
v-model="databaseData.database_hostname"
:content-loading="isFetchingInitialData"
:invalid="v$.database_hostname.$error"
/>
</BaseInputGroup>
</div>
<BaseButton
v-show="!isFetchingInitialData"
:content-loading="isFetchingInitialData"
type="submit"
class="mt-4"
:loading="isSaving"
:disabled="isSaving"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</form>
</template>
<script setup>
import { computed, onMounted, reactive, inject } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { helpers, required, numeric } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const connections = reactive(['sqlite', 'mysql', 'pgsql'])
const { t } = useI18n()
const utils = inject('utils')
const installationStore = useInstallationStore()
const databaseData = computed(() => {
return installationStore.currentDataBaseData
})
onMounted(() => {
for (const key in databaseData.value) {
if (props.configData.hasOwnProperty(key)) {
databaseData.value[key] = props.configData[key]
}
}
})
const isUrl = (value) => utils.checkValidUrl(value)
const rules = {
database_connection: {
required: helpers.withMessage(t('validation.required'), required),
},
database_hostname: {
required: helpers.withMessage(t('validation.required'), required),
},
database_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric: numeric,
},
database_name: {
required: helpers.withMessage(t('validation.required'), required),
},
database_username: {
required: helpers.withMessage(t('validation.required'), required),
},
app_url: {
required: helpers.withMessage(t('validation.required'), required),
isUrl: helpers.withMessage(t('validation.invalid_url'), isUrl),
},
}
const v$ = useVuelidate(rules, databaseData.value)
function next() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
emit('submit-data', databaseData.value)
}
function onChangeConnection() {
v$.value.database_connection.$touch()
emit('on-change-driver', databaseData.value.database_connection)
}
</script>

View File

@ -0,0 +1,146 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:content-loading="isFetchingInitialData"
:error="v$.app_url.$error && v$.app_url.$errors[0].$message"
required
>
<BaseInput
v-model="databaseData.app_url"
:content-loading="isFetchingInitialData"
:invalid="v$.app_url.$error"
type="text"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.connection')"
:content-loading="isFetchingInitialData"
:error="
v$.database_connection.$error &&
v$.database_connection.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="databaseData.database_connection"
:content-loading="isFetchingInitialData"
:invalid="v$.database_connection.$error"
:options="connections"
:can-deselect="false"
:can-clear="false"
@update:modelValue="onChangeConnection"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.database.db_path')"
:error="v$.database_name.$error && v$.database_name.$errors[0].$message"
:content-loading="isFetchingInitialData"
required
>
<BaseInput
v-model="databaseData.database_name"
:content-loading="isFetchingInitialData"
:invalid="v$.database_name.$error"
/>
</BaseInputGroup>
</div>
<BaseButton
v-show="!isFetchingInitialData"
:content-loading="isFetchingInitialData"
type="submit"
class="mt-4"
:loading="isSaving"
:disabled="isSaving"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('wizard.save_cont') }}
</BaseButton>
</form>
</template>
<script setup>
import { computed, onMounted, reactive, inject } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation'
import { helpers, required } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
const props = defineProps({
configData: {
type: Object,
require: true,
default: Object,
},
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const connections = reactive(['sqlite', 'mysql', 'pgsql'])
const { t } = useI18n()
const utils = inject('utils')
const installationStore = useInstallationStore()
const databaseData = computed(() => {
return installationStore.currentDataBaseData
})
onMounted(() => {
for (const key in databaseData.value) {
if (props.configData.hasOwnProperty(key)) {
databaseData.value[key] = props.configData[key]
}
}
})
const isUrl = (value) => utils.checkValidUrl(value)
const rules = {
database_connection: {
required: helpers.withMessage(t('validation.required'), required),
},
database_name: {
required: helpers.withMessage(t('validation.required'), required),
},
app_url: {
required: helpers.withMessage(t('validation.required'), required),
isUrl: helpers.withMessage(t('validation.invalid_url'), isUrl),
},
}
const v$ = useVuelidate(rules, databaseData.value)
function next() {
v$.value.$touch()
if (v$.value.$invalid) {
return true
}
emit('submit-data', databaseData.value)
}
function onChangeConnection() {
v$.value.database_connection.$touch()
emit('on-change-driver', databaseData.value.database_connection)
}
</script>

View File

@ -0,0 +1,147 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.mail_driver.$error &&
v$.basicMailConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="basicMailConfig.mail_driver"
:invalid="v$.basicMailConfig.mail_driver.$error"
:options="mailDriverStore.mail_drivers"
:can-deselect="false"
:content-loading="isFetchingInitialData"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.from_name.$error &&
v$.basicMailConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="basicMailConfig.from_name"
:invalid="v$.basicMailConfig.from_name.$error"
:content-loading="isFetchingInitialData"
type="text"
name="name"
@input="v$.basicMailConfig.from_name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.basicMailConfig.from_mail.$error &&
v$.basicMailConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="basicMailConfig.from_mail"
:invalid="v$.basicMailConfig.from_mail.$error"
:content-loading="isFetchingInitialData"
type="text"
@input="v$.basicMailConfig.from_mail.$touch()"
/>
</BaseInputGroup>
</div>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
:content-loading="isFetchingInitialData"
class="mt-4"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
</form>
</template>
<script setup>
import { required, email, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
import { computed } from 'vue'
const props = defineProps({
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const { t } = useI18n()
const mailDriverStore = useMailDriverStore()
const basicMailConfig = computed(() => {
return mailDriverStore.basicMailConfig
})
const mailDrivers = computed(() => {
return mailDriverStore.mail_drivers
})
basicMailConfig.value.mail_driver = 'mail'
const rules = computed(() => {
return {
basicMailConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
function saveEmailConfig() {
v$.value.$touch()
if (!v$.value.$invalid) {
emit('submit-data', mailDriverStore.basicMailConfig)
}
return false
}
function onChangeDriver() {
v$.value.basicMailConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore?.basicMailConfig?.mail_driver)
}
</script>

View File

@ -0,0 +1,237 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:mb-6 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_driver.$error &&
v$.mailgunConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="mailgunConfig.mail_driver"
:options="mailDriverStore.mail_drivers"
:can-deselect="false"
:invalid="v$.mailgunConfig.mail_driver.$error"
:content-loading="isFetchingInitialData"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.mailgun_domain')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_domain.$error &&
v$.mailgunConfig.mail_mailgun_domain.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailgunConfig.mail_mailgun_domain"
:invalid="v$.mailgunConfig.mail_mailgun_domain.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mailgun_domain"
@input="v$.mailgunConfig.mail_mailgun_domain.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:mb-6 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.mailgun_secret')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_secret.$error &&
v$.mailgunConfig.mail_mailgun_secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailgunConfig.mail_mailgun_secret"
:invalid="v$.mailgunConfig.mail_mailgun_secret.$error"
:type="getInputType"
:content-loading="isFetchingInitialData"
name="mailgun_secret"
autocomplete="off"
data-lpignore="true"
@input="v$.mailgunConfig.mail_mailgun_secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
name="EyeOffIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
name="EyeIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.mailgun_endpoint')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.mail_mailgun_endpoint.$error &&
v$.mailgunConfig.mail_mailgun_endpoint.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailgunConfig.mail_mailgun_endpoint"
:invalid="v$.mailgunConfig.mail_mailgun_endpoint.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mailgun_endpoint"
@input="v$.mailgunConfig.mail_mailgun_endpoint.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.from_mail.$error &&
v$.mailgunConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailgunConfig.from_mail"
name="from_mail"
type="text"
:invalid="v$.mailgunConfig.from_mail.$error"
:content-loading="isFetchingInitialData"
@input="v$.mailgunConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.mailgunConfig.from_name.$error &&
v$.mailgunConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="mailgunConfig.from_name"
:invalid="v$.mailgunConfig.from_name.$error"
:content-loading="isFetchingInitialData"
type="text"
name="from_name"
@input="v$.mailgunConfig.from_name.$touch()"
/>
</BaseInputGroup>
</div>
<BaseButton
:loading="loading"
:disabled="isSaving"
:content-loading="isFetchingInitialData"
class="mt-4"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
</form>
</template>
<script setup>
import { required, email, helpers } from '@vuelidate/validators'
import { useI18n } from 'vue-i18n'
import useVuelidate from '@vuelidate/core'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
import { computed, onMounted, ref } from 'vue'
const props = defineProps({
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
let isShowPassword = ref(false)
const mailDriverStore = useMailDriverStore()
const { t } = useI18n()
const mailgunConfig = computed(() => {
return mailDriverStore.mailgunConfig
})
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
mailgunConfig.value.mail_driver = 'mailgun'
const rules = computed(() => {
return {
mailgunConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_domain: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_endpoint: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_mailgun_secret: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email,
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
function saveEmailConfig() {
v$.value.$touch()
if (!v$.value.$invalid) {
emit('submit-data', mailDriverStore.mailgunConfig)
}
return false
}
function onChangeDriver() {
v$.value.mailgunConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.mailgunConfig.mail_driver)
}
</script>

View File

@ -0,0 +1,278 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_driver.$error &&
v$.sesConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="sesConfig.mail_driver"
:options="mailDriverStore.mail_drivers"
:can-deselect="false"
:content-loading="isFetchingInitialData"
:invalid="v$.sesConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.host')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_host.$error &&
v$.sesConfig.mail_host.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.mail_host"
:invalid="v$.sesConfig.mail_host.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mail_host"
@input="v$.sesConfig.mail_host.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.port')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_port.$error &&
v$.sesConfig.mail_port.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.mail_port"
:invalid="v$.sesConfig.mail_port.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mail_port"
@input="v$.sesConfig.mail_port.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.encryption')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_encryption.$error &&
v$.sesConfig.mail_encryption.$errors[0].$message
"
required
>
<BaseMultiselect
v-model.trim="sesConfig.mail_encryption"
:invalid="v$.sesConfig.mail_encryption.$error"
:options="encryptions"
:content-loading="isFetchingInitialData"
@input="v$.sesConfig.mail_encryption.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.from_mail')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.from_mail.$error &&
v$.sesConfig.from_mail.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.from_mail"
:invalid="v$.sesConfig.from_mail.$error"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
@input="v$.sesConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.from_name')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.from_name.$error &&
v$.sesConfig.from_name.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.from_name"
:invalid="v$.sesConfig.from_name.$error"
:content-loading="isFetchingInitialData"
type="text"
name="name"
@input="v$.sesConfig.from_name.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.mail.ses_key')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_ses_key.$error &&
v$.sesConfig.mail_ses_key.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.mail_ses_key"
:invalid="v$.sesConfig.mail_ses_key.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mail_ses_key"
@input="v$.sesConfig.mail_ses_key.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.ses_secret')"
:content-loading="isFetchingInitialData"
:error="
v$.sesConfig.mail_ses_secret.$error &&
v$.sesConfig.mail_ses_secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="sesConfig.mail_ses_secret"
:invalid="v$.sesConfig.mail_ses_secret.$error"
:type="getInputType"
:content-loading="isFetchingInitialData"
name="mail_ses_secret"
autocomplete="off"
data-lpignore="true"
@input="v$.sesConfig.mail_ses_secret.$touch()"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
name="EyeOffIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
name="EyeIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
</div>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
:content-loading="isFetchingInitialData"
class="mt-4"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
</form>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { required, email, numeric, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
const { t } = useI18n()
const encryptions = reactive(['tls', 'ssl', 'starttls'])
let isShowPassword = ref(false)
const mailDriverStore = useMailDriverStore()
const sesConfig = computed(() => {
return mailDriverStore.sesConfig
})
sesConfig.value.mail_driver = 'ses'
const rules = computed(() => {
return {
sesConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_host: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric,
},
mail_ses_key: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_ses_secret: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
async function saveEmailConfig() {
v$.value.$touch()
if (!v$.value.$invalid) {
emit('submit-data', mailDriverStore.sesConfig)
}
return false
}
function onChangeDriver() {
v$.value.sesConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.sesConfig.mail_driver)
}
</script>

View File

@ -0,0 +1,265 @@
<template>
<form @submit.prevent="saveEmailConfig">
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.driver')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_driver.$error &&
v$.smtpConfig.mail_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="smtpConfig.mail_driver"
:options="mailDriverStore.mail_drivers"
:can-deselect="false"
:content-loading="isFetchingInitialData"
:invalid="v$.smtpConfig.mail_driver.$error"
@update:modelValue="onChangeDriver"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.host')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_host.$error &&
v$.smtpConfig.mail_host.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="smtpConfig.mail_host"
:invalid="v$.smtpConfig.mail_host.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mail_host"
@input="v$.smtpConfig.mail_host.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.username')"
:content-loading="isFetchingInitialData"
>
<BaseInput
v-model.trim="smtpConfig.mail_username"
:content-loading="isFetchingInitialData"
type="text"
name="db_name"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.password')"
:content-loading="isFetchingInitialData"
>
<BaseInput
v-model.trim="smtpConfig.mail_password"
:type="getInputType"
:content-loading="isFetchingInitialData"
autocomplete="off"
data-lpignore="true"
name="password"
>
<template #right>
<BaseIcon
v-if="isShowPassword"
name="EyeOffIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
<BaseIcon
v-else
name="EyeIcon"
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
@click="isShowPassword = !isShowPassword"
/>
</template>
</BaseInput>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 md:mb-6">
<BaseInputGroup
:label="$t('wizard.mail.port')"
:error="
v$.smtpConfig.mail_port.$error &&
v$.smtpConfig.mail_port.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseInput
v-model.trim="smtpConfig.mail_port"
:invalid="v$.smtpConfig.mail_port.$error"
:content-loading="isFetchingInitialData"
type="text"
name="mail_port"
@input="v$.smtpConfig.mail_port.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.encryption')"
:error="
v$.smtpConfig.mail_encryption.$error &&
v$.smtpConfig.mail_encryption.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseMultiselect
v-model.trim="smtpConfig.mail_encryption"
:options="encryptions"
:can-deselect="false"
:invalid="v$.smtpConfig.mail_encryption.$error"
:content-loading="isFetchingInitialData"
@input="v$.smtpConfig.mail_encryption.$touch()"
/>
</BaseInputGroup>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2">
<BaseInputGroup
:label="$t('wizard.mail.from_mail')"
:error="
v$.smtpConfig.from_mail.$error &&
v$.smtpConfig.from_mail.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseInput
v-model.trim="smtpConfig.from_mail"
:invalid="v$.smtpConfig.from_mail.$error"
:content-loading="isFetchingInitialData"
type="text"
name="from_mail"
@input="v$.smtpConfig.from_mail.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('wizard.mail.from_name')"
:error="
v$.smtpConfig.from_name.$error &&
v$.smtpConfig.from_name.$errors[0].$message
"
:content-loading="isFetchingInitialData"
required
>
<BaseInput
v-model.trim="smtpConfig.from_name"
:invalid="v$.smtpConfig.from_name.$error"
:content-loading="isFetchingInitialData"
type="text"
name="from_name"
@input="v$.smtpConfig.from_name.$touch()"
/>
</BaseInputGroup>
</div>
<BaseButton
:loading="isSaving"
:disabled="isSaving"
:content-loading="isFetchingInitialData"
class="mt-4"
>
<template #left="slotProps">
<BaseIcon v-if="!isSaving" name="SaveIcon" :class="slotProps.class" />
</template>
{{ $t('general.save') }}
</BaseButton>
</form>
</template>
<script setup>
import { reactive, ref, computed } from 'vue'
import { required, email, numeric, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useI18n } from 'vue-i18n'
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
const props = defineProps({
isSaving: {
type: Boolean,
require: true,
default: false,
},
isFetchingInitialData: {
type: Boolean,
require: true,
default: false,
},
})
const emit = defineEmits(['submit-data', 'on-change-driver'])
let isShowPassword = ref(false)
const encryptions = reactive(['tls', 'ssl', 'starttls'])
const { t } = useI18n()
const mailDriverStore = useMailDriverStore()
const smtpConfig = computed(() => {
return mailDriverStore.smtpConfig
})
const getInputType = computed(() => {
if (isShowPassword.value) {
return 'text'
}
return 'password'
})
smtpConfig.value.mail_driver = 'smtp'
const rules = computed(() => {
return {
smtpConfig: {
mail_driver: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_host: {
required: helpers.withMessage(t('validation.required'), required),
},
mail_port: {
required: helpers.withMessage(t('validation.required'), required),
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
},
mail_encryption: {
required: helpers.withMessage(t('validation.required'), required),
},
from_mail: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
from_name: {
required: helpers.withMessage(t('validation.required'), required),
},
},
}
})
const v$ = useVuelidate(
rules,
computed(() => mailDriverStore)
)
async function saveEmailConfig() {
v$.value.$touch()
if (!v$.value.$invalid) {
emit('submit-data', mailDriverStore.smtpConfig)
}
return false
}
function onChangeDriver() {
v$.value.smtpConfig.mail_driver.$touch()
emit('on-change-driver', mailDriverStore.smtpConfig.mail_driver)
}
</script>