Delete legacy v1 frontend (resources/scripts)

The resources/scripts/ directory was the original Vue 2 / Pinia v1 admin and customer-portal SPA. It has been fully orphaned for some time — vite.config.js has zero entry points pointing at it and the only blade @vite() reference in resources/views/app.blade.php loads scripts-v2/main.ts. The directory was pure dead code.

Removes 424 .vue / .js / store / router / helper files (~2.7 MB) so that resources/scripts-v2/ can be renamed back to resources/scripts/ in a follow-up commit, dropping the v2 suffix now that there is no v1 left.
This commit is contained in:
Darko Gjorgjijoski
2026-04-07 12:48:15 +02:00
parent f83ec6e78f
commit 064bdf5395
424 changed files with 0 additions and 62746 deletions

View File

@@ -1,5 +0,0 @@
<template>
<router-view />
<BaseDialog />
</template>

View File

@@ -1,76 +0,0 @@
import { createApp } from 'vue'
import App from '@/scripts/App.vue'
import { createI18n } from 'vue-i18n'
import messages from '/lang/locales'
import router from '@/scripts/router/index'
import { defineGlobalComponents } from './global-components'
import utils from '@/scripts/helpers/utilities.js'
import _ from 'lodash'
import { VTooltip } from 'v-tooltip'
import { setI18nLanguage } from '@/scripts/helpers/language-loader.js'
const app = createApp(App)
export default class InvoiceShelf {
constructor() {
this.bootingCallbacks = []
this.messages = messages
this.i18n = null
}
booting(callback) {
this.bootingCallbacks.push(callback)
}
executeCallbacks() {
this.bootingCallbacks.forEach((callback) => {
callback(app, router)
})
}
addMessages(moduleMessages = []) {
_.merge(this.messages, moduleMessages)
}
/**
* Dynamically load and set a language
* @param {string} locale - Language code to load
* @returns {Promise<void>}
*/
async loadLanguage(locale) {
if (this.i18n) {
await setI18nLanguage(this.i18n, locale)
}
}
start() {
this.executeCallbacks()
defineGlobalComponents(app)
app.provide('$utils', utils)
this.i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
globalInjection: true,
messages: this.messages,
})
window.i18n = this.i18n
// Expose language loader globally
window.loadLanguage = this.loadLanguage.bind(this)
const { createPinia } = window.pinia
app.use(router)
app.use(this.i18n)
app.use(createPinia())
app.provide('utils', utils)
app.directive('tooltip', VTooltip)
app.mount('body')
}
}

View File

@@ -1,586 +0,0 @@
import abilities from '@/scripts/admin/stub/abilities'
const LayoutInstallation = () =>
import('@/scripts/admin/layouts/LayoutInstallation.vue')
const Login = () => import('@/scripts/admin/views/auth/Login.vue')
const LayoutBasic = () => import('@/scripts/admin/layouts/LayoutBasic.vue')
const LayoutLogin = () => import('@/scripts/admin/layouts/LayoutLogin.vue')
const ResetPassword = () =>
import('@/scripts/admin/views/auth/ResetPassword.vue')
const ForgotPassword = () =>
import('@/scripts/admin/views/auth/ForgotPassword.vue')
// Dashboard
const Dashboard = () => import('@/scripts/admin/views/dashboard/Dashboard.vue')
// Customers
const CustomerIndex = () => import('@/scripts/admin/views/customers/Index.vue')
const CustomerCreate = () =>
import('@/scripts/admin/views/customers/Create.vue')
const CustomerView = () => import('@/scripts/admin/views/customers/View.vue')
//Settings
const SettingsIndex = () =>
import('@/scripts/admin/views/settings/SettingsIndex.vue')
const UserSettingsIndex = () =>
import('@/scripts/admin/views/user-settings/UserSettingsIndex.vue')
const UserSettingsGeneral = () =>
import('@/scripts/admin/views/user-settings/GeneralTab.vue')
const UserSettingsProfilePhoto = () =>
import('@/scripts/admin/views/user-settings/ProfilePhotoTab.vue')
const UserSettingsSecurity = () =>
import('@/scripts/admin/views/user-settings/SecurityTab.vue')
const CompanyInfo = () =>
import('@/scripts/admin/views/settings/CompanyInfoSettings.vue')
const Preferences = () =>
import('@/scripts/admin/views/settings/PreferencesSetting.vue')
const Customization = () =>
import(
'@/scripts/admin/views/settings/customization/CustomizationSetting.vue'
)
const Notifications = () =>
import('@/scripts/admin/views/settings/NotificationsSetting.vue')
const TaxTypes = () =>
import('@/scripts/admin/views/settings/TaxTypesSetting.vue')
const PaymentMode = () =>
import('@/scripts/admin/views/settings/PaymentsModeSetting.vue')
const CustomFieldsIndex = () =>
import('@/scripts/admin/views/settings/CustomFieldsSetting.vue')
const NotesSetting = () =>
import('@/scripts/admin/views/settings/NotesSetting.vue')
const ExpenseCategory = () =>
import('@/scripts/admin/views/settings/ExpenseCategorySetting.vue')
const ExchangeRateSetting = () =>
import('@/scripts/admin/views/settings/ExchangeRateProviderSetting.vue')
const RolesSettings = () =>
import('@/scripts/admin/views/settings/RolesSettings.vue')
const CompanyMailConfig = () =>
import('@/scripts/admin/views/settings/CompanyMailConfigSetting.vue')
// Items
const ItemsIndex = () => import('@/scripts/admin/views/items/Index.vue')
const ItemCreate = () => import('@/scripts/admin/views/items/Create.vue')
// Expenses
const ExpensesIndex = () => import('@/scripts/admin/views/expenses/Index.vue')
const ExpenseCreate = () => import('@/scripts/admin/views/expenses/Create.vue')
// Members
const MemberIndex = () => import('@/scripts/admin/views/members/Index.vue')
// Estimates
const EstimateIndex = () => import('@/scripts/admin/views/estimates/Index.vue')
const EstimateCreate = () =>
import('@/scripts/admin/views/estimates/create/EstimateCreate.vue')
const EstimateView = () => import('@/scripts/admin/views/estimates/View.vue')
// Payments
const PaymentsIndex = () => import('@/scripts/admin/views/payments/Index.vue')
const PaymentCreate = () => import('@/scripts/admin/views/payments/Create.vue')
const PaymentView = () => import('@/scripts/admin/views/payments/View.vue')
const NotFoundPage = () => import('@/scripts/admin/views/errors/404.vue')
// Invoice
const InvoiceIndex = () => import('@/scripts/admin/views/invoices/Index.vue')
const InvoiceCreate = () =>
import('@/scripts/admin/views/invoices/create/InvoiceCreate.vue')
const InvoiceView = () => import('@/scripts/admin/views/invoices/View.vue')
// Recurring Invoice
const RecurringInvoiceIndex = () =>
import('@/scripts/admin/views/recurring-invoices/Index.vue')
const RecurringInvoiceCreate = () =>
import(
'@/scripts/admin/views/recurring-invoices/create/RecurringInvoiceCreate.vue'
)
const RecurringInvoiceView = () =>
import('@/scripts/admin/views/recurring-invoices/View.vue')
// Reports
const ReportsIndex = () =>
import('@/scripts/admin/views/reports/layout/Index.vue')
// Installation
const Installation = () =>
import('@/scripts/admin/views/installation/Installation.vue')
// Modules
const ModuleIndex = () => import('@/scripts/admin/views/modules/Index.vue')
const ModuleView = () => import('@/scripts/admin/views/modules/View.vue')
const InvoicePublicPage = () =>
import('@/scripts/components/InvoicePublicPage.vue')
// Administration (Super Admin)
const AdminDashboard = () =>
import('@/scripts/admin/views/administration/AdminDashboard.vue')
const AdminCompaniesIndex = () =>
import('@/scripts/admin/views/administration/companies/Index.vue')
const AdminCompaniesEdit = () =>
import('@/scripts/admin/views/administration/companies/Edit.vue')
const AdminUsersIndex = () =>
import('@/scripts/admin/views/administration/users/Index.vue')
const AdminUsersEdit = () =>
import('@/scripts/admin/views/administration/users/Edit.vue')
const AdminSettingsIndex = () =>
import('@/scripts/admin/views/administration/settings/SettingsIndex.vue')
const AdminMailConfig = () =>
import('@/scripts/admin/views/settings/MailConfigSetting.vue')
const AdminPDFGeneration = () =>
import('@/scripts/admin/views/settings/PDFGenerationSetting.vue')
const AdminBackup = () =>
import('@/scripts/admin/views/settings/BackupSetting.vue')
const AdminUpdateApp = () =>
import('@/scripts/admin/views/settings/UpdateAppSetting.vue')
const AdminFileDisk = () =>
import('@/scripts/admin/views/settings/FileDiskSetting.vue')
const NoCompanyView = () =>
import('@/scripts/admin/views/NoCompanyView.vue')
const RegisterWithInvitation = () =>
import('@/scripts/admin/views/auth/RegisterWithInvitation.vue')
export default [
{
path: '/register',
name: 'register',
component: RegisterWithInvitation,
meta: { requiresAuth: false },
},
{
path: '/installation',
component: LayoutInstallation,
meta: { requiresAuth: false },
children: [
{
path: '/installation',
component: Installation,
name: 'installation',
},
],
},
{
path: '/customer/invoices/view/:hash',
component: InvoicePublicPage,
name: 'invoice.public',
},
{
path: '/',
component: LayoutLogin,
meta: { requiresAuth: false, redirectIfAuthenticated: true },
children: [
{
path: '',
component: Login,
},
{
path: 'login',
name: 'login',
component: Login,
},
{
path: 'forgot-password',
component: ForgotPassword,
name: 'forgot-password',
},
{
path: '/reset-password/:token',
component: ResetPassword,
name: 'reset-password',
},
],
},
{
path: '/admin',
component: LayoutBasic,
meta: { requiresAuth: true },
children: [
{
path: 'no-company',
name: 'no.company',
component: NoCompanyView,
},
{
path: 'dashboard',
name: 'dashboard',
meta: { ability: abilities.DASHBOARD },
component: Dashboard,
},
// Customers
{
path: 'customers',
meta: { ability: abilities.VIEW_CUSTOMER },
component: CustomerIndex,
},
{
path: 'customers/create',
name: 'customers.create',
meta: { ability: abilities.CREATE_CUSTOMER },
component: CustomerCreate,
},
{
path: 'customers/:id/edit',
name: 'customers.edit',
meta: { ability: abilities.EDIT_CUSTOMER },
component: CustomerCreate,
},
{
path: 'customers/:id/view',
name: 'customers.view',
meta: { ability: abilities.VIEW_CUSTOMER },
component: CustomerView,
},
// Payments
{
path: 'payments',
meta: { ability: abilities.VIEW_PAYMENT },
component: PaymentsIndex,
},
{
path: 'payments/create',
name: 'payments.create',
meta: { ability: abilities.CREATE_PAYMENT },
component: PaymentCreate,
},
{
path: 'payments/:id/create',
name: 'invoice.payments.create',
meta: { ability: abilities.CREATE_PAYMENT },
component: PaymentCreate,
},
{
path: 'payments/:id/edit',
name: 'payments.edit',
meta: { ability: abilities.EDIT_PAYMENT },
component: PaymentCreate,
},
{
path: 'payments/:id/view',
name: 'payments.view',
meta: { ability: abilities.VIEW_PAYMENT },
component: PaymentView,
},
// user settings
{
path: 'user-settings',
name: 'user.settings',
component: UserSettingsIndex,
children: [
{
path: 'general',
name: 'user.settings.general',
component: UserSettingsGeneral,
},
{
path: 'profile-photo',
name: 'user.settings.profile-photo',
component: UserSettingsProfilePhoto,
},
{
path: 'security',
name: 'user.settings.security',
component: UserSettingsSecurity,
},
],
},
//settings
{
path: 'settings',
name: 'settings',
component: SettingsIndex,
children: [
{
path: 'company-info',
name: 'company.info',
meta: { isOwner: true },
component: CompanyInfo,
},
{
path: 'preferences',
name: 'preferences',
meta: { isOwner: true },
component: Preferences,
},
{
path: 'customization',
name: 'customization',
meta: { isOwner: true },
component: Customization,
},
{
path: 'notifications',
name: 'notifications',
meta: { isOwner: true },
component: Notifications,
},
{
path: 'roles-settings',
name: 'roles.settings',
meta: { isOwner: true },
component: RolesSettings,
},
{
path: 'exchange-rate-provider',
name: 'exchange.rate.provider',
meta: { ability: abilities.VIEW_EXCHANGE_RATE },
component: ExchangeRateSetting,
},
{
path: 'tax-types',
name: 'tax.types',
meta: { ability: abilities.VIEW_TAX_TYPE },
component: TaxTypes,
},
{
path: 'notes',
name: 'notes',
meta: { ability: abilities.VIEW_ALL_NOTES },
component: NotesSetting,
},
{
path: 'payment-mode',
name: 'payment.mode',
component: PaymentMode,
},
{
path: 'custom-fields',
name: 'custom.fields',
meta: { ability: abilities.VIEW_CUSTOM_FIELDS },
component: CustomFieldsIndex,
},
{
path: 'expense-category',
name: 'expense.category',
meta: { ability: abilities.VIEW_EXPENSE },
component: ExpenseCategory,
},
{
path: 'mail-configuration',
name: 'company.mailconfig',
meta: { isOwner: true },
component: CompanyMailConfig,
},
],
},
// Items
{
path: 'items',
meta: { ability: abilities.VIEW_ITEM },
component: ItemsIndex,
},
{
path: 'items/create',
name: 'items.create',
meta: { ability: abilities.CREATE_ITEM },
component: ItemCreate,
},
{
path: 'items/:id/edit',
name: 'items.edit',
meta: { ability: abilities.EDIT_ITEM },
component: ItemCreate,
},
// Expenses
{
path: 'expenses',
meta: { ability: abilities.VIEW_EXPENSE },
component: ExpensesIndex,
},
{
path: 'expenses/create',
name: 'expenses.create',
meta: { ability: abilities.CREATE_EXPENSE },
component: ExpenseCreate,
},
{
path: 'expenses/:id/edit',
name: 'expenses.edit',
meta: { ability: abilities.EDIT_EXPENSE },
component: ExpenseCreate,
},
// Members
{
path: 'members',
name: 'members.index',
meta: { isOwner: true },
component: MemberIndex,
},
// Estimates
{
path: 'estimates',
name: 'estimates.index',
meta: { ability: abilities.VIEW_ESTIMATE },
component: EstimateIndex,
},
{
path: 'estimates/create',
name: 'estimates.create',
meta: { ability: abilities.CREATE_ESTIMATE },
component: EstimateCreate,
},
{
path: 'estimates/:id/view',
name: 'estimates.view',
meta: { ability: abilities.VIEW_ESTIMATE },
component: EstimateView,
},
{
path: 'estimates/:id/edit',
name: 'estimates.edit',
meta: { ability: abilities.EDIT_ESTIMATE },
component: EstimateCreate,
},
// Invoices
{
path: 'invoices',
name: 'invoices.index',
meta: { ability: abilities.VIEW_INVOICE },
component: InvoiceIndex,
},
{
path: 'invoices/create',
name: 'invoices.create',
meta: { ability: abilities.CREATE_INVOICE },
component: InvoiceCreate,
},
{
path: 'invoices/:id/view',
name: 'invoices.view',
meta: { ability: abilities.VIEW_INVOICE },
component: InvoiceView,
},
{
path: 'invoices/:id/edit',
name: 'invoices.edit',
meta: { ability: abilities.EDIT_INVOICE },
component: InvoiceCreate,
},
// Recurring Invoices
{
path: 'recurring-invoices',
name: 'recurring-invoices.index',
meta: { ability: abilities.VIEW_RECURRING_INVOICE },
component: RecurringInvoiceIndex,
},
{
path: 'recurring-invoices/create',
name: 'recurring-invoices.create',
meta: { ability: abilities.CREATE_RECURRING_INVOICE },
component: RecurringInvoiceCreate,
},
{
path: 'recurring-invoices/:id/view',
name: 'recurring-invoices.view',
meta: { ability: abilities.VIEW_RECURRING_INVOICE },
component: RecurringInvoiceView,
},
{
path: 'recurring-invoices/:id/edit',
name: 'recurring-invoices.edit',
meta: { ability: abilities.EDIT_RECURRING_INVOICE },
component: RecurringInvoiceCreate,
},
// Modules
{
path: 'modules',
name: 'modules.index',
meta: { isOwner: true },
component: ModuleIndex,
},
{
path: 'modules/:slug',
name: 'modules.view',
meta: { isOwner: true },
component: ModuleView,
},
// Reports
{
path: 'reports',
meta: { ability: abilities.VIEW_FINANCIAL_REPORT },
component: ReportsIndex,
},
// Administration (Super Admin)
{
path: 'administration/dashboard',
name: 'admin.dashboard',
meta: { isSuperAdmin: true },
component: AdminDashboard,
},
{
path: 'administration/companies',
name: 'admin.companies.index',
meta: { isSuperAdmin: true },
component: AdminCompaniesIndex,
},
{
path: 'administration/companies/:id/edit',
name: 'admin.companies.edit',
meta: { isSuperAdmin: true },
component: AdminCompaniesEdit,
},
{
path: 'administration/users',
name: 'admin.users.index',
meta: { isSuperAdmin: true },
component: AdminUsersIndex,
},
{
path: 'administration/users/:id/edit',
name: 'admin.users.edit',
meta: { isSuperAdmin: true },
component: AdminUsersEdit,
},
{
path: 'administration/settings',
name: 'admin.settings',
meta: { isSuperAdmin: true },
component: AdminSettingsIndex,
children: [
{
path: 'mail-configuration',
name: 'admin.settings.mail',
component: AdminMailConfig,
},
{
path: 'pdf-generation',
name: 'admin.settings.pdf',
component: AdminPDFGeneration,
},
{
path: 'backup',
name: 'admin.settings.backup',
component: AdminBackup,
},
{
path: 'update-app',
name: 'admin.settings.update',
component: AdminUpdateApp,
},
{
path: 'file-disk',
name: 'admin.settings.filedisk',
component: AdminFileDisk,
},
],
},
],
},
{ path: '/:catchAll(.*)', component: NotFoundPage },
]

View File

@@ -1,98 +0,0 @@
<template>
<div
class="
relative
flex
px-4
py-2
rounded-lg
bg-surface-muted/40
whitespace-nowrap
flex-col
mt-1
"
>
<span
ref="publicUrl"
class="
pr-10
text-sm
font-medium
text-heading
truncate
select-all select-color
"
>
{{ token }}
</span>
<svg
v-tooltip="{ content: $t('general.copy_to_clipboard') }"
class="
absolute
right-0
h-full
inset-y-0
cursor-pointer
focus:outline-hidden
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

@@ -1,41 +0,0 @@
<template>
<div
v-if="isImpersonating"
class="fixed top-0 left-0 right-0 z-50 flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-orange-600"
>
<BaseIcon name="ExclamationTriangleIcon" class="w-4 h-4 mr-2" />
<span>{{ $t('administration.users.impersonating_banner') }}</span>
<button
class="ml-4 px-3 py-1 text-xs font-semibold text-orange-600 bg-white rounded hover:bg-orange-50"
:disabled="isStopping"
@click="stopImpersonating"
>
{{ $t('administration.users.stop_impersonating') }}
</button>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import Ls from '@/scripts/services/ls.js'
import { useAdministrationStore } from '@/scripts/admin/stores/administration'
const administrationStore = useAdministrationStore()
let isStopping = ref(false)
const isImpersonating = computed(() => {
return Ls.get('admin.impersonating') === 'true'
})
async function stopImpersonating() {
isStopping.value = true
try {
await administrationStore.stopImpersonating()
} catch {
// Token already cleaned up in store action
}
window.location.href = '/admin/administration/users'
}
</script>

View File

@@ -1,209 +0,0 @@
<template>
<NoteModal />
<div class="w-full">
<Popover v-slot="{ isOpen }">
<PopoverButton
v-if="userStore.hasAbilities(abilities.VIEW_NOTE)"
:class="isOpen ? '' : ''"
class="
flex
items-center
z-10
font-medium
text-primary-400
focus:outline-hidden 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/5
"
>
<div class="relative grid bg-surface">
<div class="relative p-4">
<BaseInput
v-model="textSearch"
:placeholder="$t('general.search')"
type="text"
class="text-heading"
>
</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-line-default border-solid
cursor-pointer
hover:bg-surface-tertiary 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-body
cursor-pointer
"
>
{{ note.name }}
</label>
</div>
</div>
</div>
<div v-else class="flex justify-center p-5 text-subtle">
<label class="text-base text-muted">
{{ $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-surface-muted
border-none
outline-hidden
"
@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

@@ -1,211 +0,0 @@
<template>
<div class="graph-container h-[300px]">
<canvas id="graph" ref="graph" />
</div>
</template>
<script setup>
import { Chart } from 'chart.js/auto'
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')
const style = getComputedStyle(document.documentElement)
const gridColor = style.getPropertyValue('--color-line-light').trim() || 'rgba(0,0,0,0.1)'
const tickColor = style.getPropertyValue('--color-muted').trim() || '#6b7280'
let options = reactive({
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: { color: gridColor },
ticks: { color: tickColor },
},
y: {
grid: { color: gridColor },
ticks: { color: tickColor },
},
},
plugins: {
tooltip: {
enabled: true,
callbacks: {
label: function (context) {
return utils.formatMoney(
Math.round(context.parsed.y * 100),
defaultCurrency.value
)
},
},
},
legend: {
display: false,
},
},
})
let data = reactive({
labels: props.labels,
datasets: [
{
label: 'Sales',
fill: false,
tension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: tickColor,
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: tickColor,
pointBackgroundColor: style.getPropertyValue('--color-surface').trim() || '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: tickColor,
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: props.invoices.map((invoice) => invoice / 100),
},
{
label: 'Receipts',
fill: false,
tension: 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: style.getPropertyValue('--color-surface').trim() || '#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,
tension: 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: style.getPropertyValue('--color-surface').trim() || '#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,
tension: 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: style.getPropertyValue('--color-surface').trim() || '#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('none')
}
</script>

View File

@@ -1,118 +0,0 @@
<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-muted" style="max-width: 680px">
{{
$t('settings.exchange_rate.description', {
currency: companyStore.selectedCompanyCurrency.name,
})
}}
</p>
<form id="bulk-update-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-muted sm:text-sm">
{{ companyStore.selectedCompanyCurrency.code }}
</span>
</template>
</BaseInput>
<span class="text-subtle text-xs mt-2 font-light">
{{
$t('settings.exchange_rate.exchange_help_text', {
currency: c.code,
baseCurrency: companyStore.selectedCompanyCurrency.code,
})
}}
</span>
</BaseInputGroup>
</template>
</ValidateEach>
</form>
<template #footer>
<div
class="
z-0
flex
justify-end
"
>
<BaseButton :loading="isSaving" variant="primary" type="submit" form="bulk-update-form">
{{ $t('general.save') }}
</BaseButton>
</div>
</template>
</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'
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

@@ -1,118 +0,0 @@
<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

@@ -1,72 +0,0 @@
<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

@@ -1,24 +0,0 @@
<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

@@ -1,24 +0,0 @@
<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

@@ -1,45 +0,0 @@
<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

@@ -1,23 +0,0 @@
<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

@@ -1,23 +0,0 @@
<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

@@ -1,23 +0,0 @@
<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

@@ -1,25 +0,0 @@
<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

@@ -1,31 +0,0 @@
<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

@@ -1,24 +0,0 @@
<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

@@ -1,23 +0,0 @@
<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

@@ -1,34 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</template>
<router-link :to="`/admin/administration/companies/${row.id}/edit`">
<BaseDropdownItem>
<BaseIcon
name="PencilIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('general.edit') }}
</BaseDropdownItem>
</router-link>
</BaseDropdown>
</template>
<script setup>
defineProps({
row: {
type: Object,
default: null,
},
table: {
type: Object,
default: null,
},
loadData: {
type: Function,
default: null,
},
})
</script>

View File

@@ -1,77 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</template>
<router-link :to="`/admin/administration/users/${row.id}/edit`">
<BaseDropdownItem>
<BaseIcon
name="PencilIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('general.edit') }}
</BaseDropdownItem>
</router-link>
<BaseDropdownItem
v-if="row.id !== userStore.currentUser?.id"
@click="onImpersonate"
>
<BaseIcon
name="ArrowRightEndOnRectangleIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('administration.users.impersonate') }}
</BaseDropdownItem>
</BaseDropdown>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useAdministrationStore } from '@/scripts/admin/stores/administration'
import { useDialogStore } from '@/scripts/stores/dialog'
const props = defineProps({
row: {
type: Object,
default: null,
},
table: {
type: Object,
default: null,
},
loadData: {
type: Function,
default: null,
},
})
const userStore = useUserStore()
const administrationStore = useAdministrationStore()
const dialogStore = useDialogStore()
const { t } = useI18n()
function onImpersonate() {
dialogStore
.openDialog({
title: t('general.are_you_sure'),
message: t('administration.users.impersonate_confirm', {
name: props.row.name,
}),
yesLabel: t('administration.users.impersonate'),
noLabel: t('general.cancel'),
variant: 'danger',
size: 'lg',
hideNoButton: false,
})
.then((confirmed) => {
if (confirmed) {
administrationStore.impersonateUser(props.row.id).then(() => {
window.location.href = '/admin/dashboard'
})
}
})
}
</script>

View File

@@ -1,99 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,113 +0,0 @@
<template>
<BaseDropdown :content-loading="customerStore.isFetchingViewData">
<template #activator>
<BaseButton v-if="route.name === 'customers.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,369 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'estimates.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="text-white" />
</BaseButton>
<BaseIcon v-else class="text-muted" name="EllipsisHorizontalIcon" />
</template>
<!-- Copy PDF url -->
<BaseDropdownItem
v-if="route.name === 'estimates.view'"
@click="copyPdfUrl"
>
<BaseIcon
name="LinkIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $t('general.view') }}
</BaseDropdownItem>
</router-link>
<!-- Clone Estimate into new estimate -->
<BaseDropdownItem
v-if="userStore.hasAbilities(abilities.CREATE_ESTIMATE)"
@click="cloneEstimateData(row)"
>
<BaseIcon
name="DocumentTextIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('estimates.clone_estimate') }}
</BaseDropdownItem>
<!-- 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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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'),
})
}
async function cloneEstimateData(data) {
dialogStore
.openDialog({
title: t('general.are_you_sure'),
message: t('estimates.confirm_clone'),
yesLabel: t('general.ok'),
noLabel: t('general.cancel'),
variant: 'primary',
hideNoButton: false,
size: 'lg',
})
.then((res) => {
if (res) {
estimateStore.cloneEstimate(data).then((res) => {
router.push(`/admin/estimates/${res.data.data.id}/edit`)
})
}
})
}
</script>

View File

@@ -1,105 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton
v-if="route.name === 'expenseCategorys.view'"
variant="primary"
>
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,94 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'expenses.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,261 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'invoices.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,96 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'items.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,87 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'members.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</template>
<!-- edit user -->
<router-link :to="`/admin/members/${row.id}/edit`">
<BaseDropdownItem>
<BaseIcon
name="PencilIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('general.edit') }}
</BaseDropdownItem>
</router-link>
<!-- delete user -->
<BaseDropdownItem @click="removeUser(row.id)">
<BaseIcon
name="TrashIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $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 { useMembersStore } from '@/scripts/admin/stores/members'
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 = useMembersStore()
const $utils = inject('utils')
function removeUser(id) {
dialogStore
.openDialog({
title: t('general.are_you_sure'),
message: t('members.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

@@ -1,109 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'notes.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,166 +0,0 @@
<template>
<BaseDropdown :content-loading="contentLoading">
<template #activator>
<BaseButton v-if="route.name === 'payments.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,93 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'paymentModes.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</template>
<!-- edit paymentMode -->
<BaseDropdownItem @click="editPaymentMode(row.id)">
<BaseIcon
name="PencilIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('general.edit') }}
</BaseDropdownItem>
<!-- delete paymentMode -->
<BaseDropdownItem @click="removePaymentMode(row.id)">
<BaseIcon
name="TrashIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,131 +0,0 @@
<template>
<BaseDropdown :content-loading="recurringInvoiceStore.isFetchingViewData">
<template #activator>
<BaseButton
v-if="route.name === 'recurring-invoices.view'"
variant="primary"
>
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,106 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'roles.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,104 +0,0 @@
<template>
<BaseDropdown>
<template #activator>
<BaseButton v-if="route.name === 'tax-types.view'" variant="primary">
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
</BaseButton>
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
</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-subtle group-hover:text-muted"
/>
{{ $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-subtle group-hover:text-muted"
/>
{{ $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

@@ -1,483 +0,0 @@
<template>
<tr class="box-border bg-surface border-b border-line-light">
<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 mr-2 text-subtle cursor-move handle"
>
<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
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 ml-1 text-muted"
/>
</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-body 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 border border-line-light rounded-md"
/>
</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"
:discount="discount"
@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
return price / 100
},
set: (newValue) => {
let price = Math.round(newValue * 100)
updateItemAttribute('price', price)
setDiscount()
},
})
const subtotal = computed(() => Math.round(props.itemData.price * props.itemData.quantity))
const discount = computed({
get: () => {
return props.itemData.discount
},
set: (newValue) => {
updateItemAttribute('discount', newValue)
setDiscount()
},
})
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.amount) {
return tax.amount
}
return 0
})
)
})
const totalTax = computed(() => totalSimpleTax.value)
const rules = {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
quantity: {
required: helpers.withMessage(t('validation.required'), required),
maxLength: helpers.withMessage(
t('validation.amount_maxlength'),
maxLength(20)
),
},
price: {
required: helpers.withMessage(t('validation.required'), required),
maxLength: helpers.withMessage(
t('validation.price_maxlength'),
maxLength(20)
),
},
discount_val: {
between: helpers.withMessage(
t('validation.discount_maxlength'),
between(
0,
computed(() => Math.abs(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 setDiscount() {
const newValue = props.store[props.storeProp].items[props.index].discount
const absoluteSubtotal = Math.abs(subtotal.value)
if (props.itemData.discount_type === 'percentage'){
updateItemAttribute('discount_val', Math.round((absoluteSubtotal * newValue) / 100))
} else {
updateItemAttribute('discount_val', Math.min(Math.round(newValue * 100), absoluteSubtotal))
}
}
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,
totalTax: totalTax.value,
tax: totalTax.value,
taxes: [...itemTaxes],
tax_type_ids: itemTaxes.flatMap(_t =>
_t.tax_type_id ? _t.tax_type_id : [],
),
}
props.store.updateItem(data)
}
function updateItemAttribute(attribute, value) {
props.store.$patch((state) => {
state[props.storeProp].items[props.index][attribute] = value
})
syncItemToStore()
}
</script>

View File

@@ -1,285 +0,0 @@
<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 }} -
<template v-if="value.calculation_type === 'fixed'">
<BaseFormatMoney :amount="value.fixed_amount" :currency="currency" />
</template>
<template v-else>
{{ value.percent }} %
</template>
</div>
</template>
<template #option="{ option }">
{{ option.name }} -
<template v-if="option.calculation_type === 'fixed'">
<BaseFormatMoney :amount="option.fixed_amount" :currency="currency" />
</template>
<template v-else>
{{ option.percent }} %
</template>
</template>
<template v-if="userStore.hasAbilities(ability)" #action>
<button
type="button"
class="flex items-center justify-center w-full px-2 py-2 bg-surface-muted border-none outline-hidden cursor-pointer "
@click="openTaxModal"
>
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
<label
class="ml-2 text-sm leading-none cursor-pointer text-primary-400"
>{{ $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-body 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,
},
discountedTotal: {
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.calculation_type === 'fixed') {
return localTax.fixed_amount
}
if (props.discountedTotal) {
const taxPerItemEnabled = props.store[props.storeProp].tax_per_item === 'YES'
const discountPerItemEnabled = props.store[props.storeProp].discount_per_item === 'YES'
if (taxPerItemEnabled && !discountPerItemEnabled){
return getTaxAmount()
}
if (props.store[props.storeProp].tax_included) {
return Math.round(props.discountedTotal - (props.discountedTotal / (1 + (localTax.percent / 100))))
}
return (props.discountedTotal * localTax.percent) / 100
}
return 0
})
watch(
() => props.discountedTotal,
() => {
updateRowTax()
}
)
watch(
() => props.totalTax,
() => {
updateRowTax()
}
)
watch(
() => taxAmount.value,
() => {
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.calculation_type = val.calculation_type
localTax.percent = val.calculation_type === 'percentage' ? val.percent : null
localTax.fixed_amount = val.calculation_type === 'fixed' ? val.fixed_amount : null
localTax.tax_type_id = val.id
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)
state[props.storeProp].items[props.itemIndex].tax = 0
state[props.storeProp].items[props.itemIndex].totalTax = 0
})
}
function getTaxAmount() {
if (localTax.calculation_type === 'fixed') {
return localTax.fixed_amount
}
let total = 0
let discount = 0
const itemTotal = props.discountedTotal
const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0
const type = props.store[props.storeProp].discount_type
let discountedTotal = props.discountedTotal
if (modelDiscount > 0) {
props.store[props.storeProp].items.forEach((_i) => {
total += _i.total
})
const proportion = (itemTotal / total).toFixed(2)
discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100
const itemDiscount = Math.round(discount * proportion)
discountedTotal = itemTotal - itemDiscount
}
if (props.store[props.storeProp].tax_included) {
return Math.round(discountedTotal - (discountedTotal / (1 + (localTax.percent / 100))))
}
return Math.round((discountedTotal * localTax.percent) / 100)
}
</script>

View File

@@ -1,229 +0,0 @@
<template>
<div class="rounded-xl border border-line-light shadow overflow-hidden bg-surface">
<!-- Tax Included -->
<div
v-if="companyStore.selectedCompanySettings.tax_included === 'YES'"
class="
flex
items-center
justify-end
w-full
px-6
text-base
border-b border-line-light
cursor-pointer
text-primary-400
bg-surface
"
>
<BaseSwitchSection
v-model="taxIncludedField"
:title="$t('settings.tax_types.tax_included')"
:store="store"
:store-prop="storeProp"
/>
</div>
<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-surface-secondary border-b border-line-light">
<tr>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-left text-body
"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<span v-else class="pl-7">
{{ $t('items.item', 2) }}
</span>
</th>
<th
class="
px-5
py-3
text-sm
not-italic
font-medium
leading-5
text-right text-body
"
>
<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-body
"
>
<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-body
"
>
<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-body
"
>
<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-t border-line-light
cursor-pointer
text-primary-400
hover:bg-primary-100
"
@click="store.addItem"
>
<BaseIcon name="PlusCircleIcon" class="mr-2" />
{{ $t('general.add_new_item') }}
</div>
</div><!-- end rounded wrapper -->
</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
}
})
const taxIncludedField = computed({
get: () => {
return props.store[props.storeProp].tax_included
},
set: async (value) => {
props.store[props.storeProp].tax_included = value
},
})
</script>

View File

@@ -1,46 +0,0 @@
<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-heading 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

@@ -1,402 +0,0 @@
<template>
<div
class="
px-5
py-4
mt-6
bg-surface
border border-line-light border-solid
rounded-xl shadow
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-subtle 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-heading uppercase "
>
<BaseFormatMoney
:amount="store.getSubTotal"
:currency="defaultCurrency"
/>
</label>
</div>
<div
v-if="store[storeProp].tax_per_item === 'YES'"
>
<NetTotal
:currency="currency"
:store="store"
:storeProp="storeProp"
:isLoading="isLoading"
/>
</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-muted uppercase"
>
<template v-if="tax.calculation_type === 'percentage'">
{{ tax.name }} - {{ tax.percent }}%
</template>
<template v-else>
{{ tax.name }} - <BaseFormatMoney :amount="tax.fixed_amount" :currency="defaultCurrency" />
</template>
</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-heading 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-subtle uppercase"
>
{{ $t('estimates.discount') }}
</label>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText
:lines="1"
class="w-24 h-8 border border-line-light rounded-md"
/>
</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="p-2 rounded-none rounded-tr-md rounded-br-md"
type="button"
variant="white"
>
<span class="flex items-center">
{{
store[storeProp].discount_type == 'fixed'
? defaultCurrency.symbol
: '%'
}}
<BaseIcon
name="ChevronDownIcon"
class="w-4 h-4 ml-1 text-muted"
/>
</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
"
class="flex items-center justify-between w-full mt-2"
>
<NetTotal
:currency="currency"
:store="store"
:storeProp="storeProp"
:isLoading="isLoading"
/>
</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"
:storeProp="storeProp"
@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-line-light 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-subtle 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, watch } from 'vue'
import Guid from 'guid'
import NetTotal from './NetTotal.vue'
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()
watch(
() => props.store[props.storeProp].items,
(val) => {
setDiscount()
}, { deep: true },
)
const totalDiscount = computed({
get: () => {
return props.store[props.storeProp].discount
},
set: (newValue) => {
props.store[props.storeProp].discount = newValue
setDiscount()
},
})
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: Math.round(tax.amount),
percent: tax.percent,
name: tax.name,
calculation_type: tax.calculation_type,
fixed_amount: tax.fixed_amount
})
}
})
}
})
return taxes
})
const defaultCurrency = computed(() => {
if (props.currency) {
return props.currency
} else {
return companyStore.selectedCompanyCurrency
}
})
function setDiscount() {
const newValue = props.store[props.storeProp].discount
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * newValue) / 100)
return
}
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
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
}
const val = Math.round(props.store[props.storeProp].discount * 100) / 100
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * val) / 100)
props.store[props.storeProp].discount_type = 'percentage'
}
function onSelectTax(selectedTax) {
let amount = 0
if (selectedTax.calculation_type === 'percentage' && props.store.getSubtotalWithDiscount && selectedTax.percent) {
amount = Math.round(
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
)
} else if (selectedTax.calculation_type === 'fixed') {
amount = selectedTax.fixed_amount
}
let data = {
...TaxStub,
id: Guid.raw(),
name: selectedTax.name,
percent: selectedTax.percent,
tax_type_id: selectedTax.id,
amount,
calculation_type: selectedTax.calculation_type,
fixed_amount: selectedTax.fixed_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

@@ -1,108 +0,0 @@
<template>
<div class="flex items-center justify-between w-full mt-2 text-sm">
<label v-if="tax.calculation_type === 'percentage'" class="font-semibold leading-5 text-muted uppercase">
{{ tax.name }} ({{ tax.percent }} %)
</label>
<label v-else class="font-semibold leading-5 text-muted uppercase">
{{ tax.name }} (<BaseFormatMoney :amount="tax.fixed_amount" :currency="currency" />)
</label>
<label class="flex items-center justify-center text-lg text-heading">
<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,
},
storeProp: {
type: String,
default: '',
},
data: {
type: String,
default: '',
},
})
const emit = defineEmits(['update', 'remove'])
const utils = inject('$utils')
const taxAmount = computed(() => {
if (props.tax.calculation_type === 'fixed') {
return props.tax.fixed_amount;
}
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 && props.store[props.storeProp].tax_included) {
return Math.round(
props.store.getSubtotalWithDiscount - (
props.store.getSubtotalWithDiscount / (1 + (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()
}
})
watch(
() => props.store[props.storeProp].tax_included,
(val) => {
updateTax()
}, { deep: true },
)
function updateTax() {
emit('update', {
...props.tax,
amount: taxAmount.value,
})
}
</script>

View File

@@ -1,178 +0,0 @@
<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="ArrowPathIcon"
:class="`h-4 w-4 text-primary-500 cursor-pointer outline-hidden ${
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-muted sm:text-sm">
{{ companyCurrency.code }}
</span>
</template>
</BaseInput>
<span class="text-subtle 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

@@ -1,54 +0,0 @@
<template>
<div
v-if="store[storeProp].tax_included"
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-muted uppercase"
>
{{ $t('estimates.net_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-heading uppercase "
>
<BaseFormatMoney
:amount="store.getNetTotal"
:currency="currency"
/>
</label>
</div>
</template>
<script setup>
import BaseContentPlaceholdersText from '@/scripts/components/base/BaseContentPlaceholdersText.vue'
import BaseContentPlaceholders from '@/scripts/components/base/BaseContentPlaceholders.vue'
import BaseFormatMoney from '@/scripts/components/base/BaseFormatMoney.vue'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
currency: {
type: [Object, String],
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
})
</script>

View File

@@ -1,216 +0,0 @@
<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']),
}
}
return null
})
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

@@ -1,243 +0,0 @@
<template>
<div class="w-full mt-4 tax-select">
<Popover v-slot="{ isOpen }" class="relative">
<PopoverButton
:class="isOpen ? '' : ''"
class="
flex
items-center
text-sm
font-medium
text-primary-400
focus:outline-hidden 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-xl
shadow
ring-1 ring-black/5
"
>
<!-- Tax Search Input -->
<div class="relative bg-surface">
<div class="relative p-4">
<BaseInput
v-model="textSearch"
:placeholder="$t('general.search')"
type="text"
class="text-heading"
>
</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-line-light
"
>
<div
v-for="(taxType, index) in filteredTaxType"
:key="index"
:class="{
'bg-surface-tertiary 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-line-light border-solid
cursor-pointer
hover:bg-surface-tertiary 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-body
cursor-pointer
"
>
{{ taxType.name }}
</label>
<label
class="
m-0
text-base
font-semibold
text-body
cursor-pointer
"
>
<template v-if="taxType.calculation_type === 'fixed'">
<BaseFormatMoney :amount="taxType.fixed_amount" :currency="companyStore.selectedCompanyCurrency" />
</template>
<template v-else>
{{ taxType.percent }} %
</template>
</label>
</div>
</div>
</div>
<div v-else class="flex justify-center p-5 text-subtle">
<label class="text-base text-muted 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-surface-muted
border-none
outline-hidden
"
@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 { useCompanyStore } from '@/scripts/admin/stores/company'
import abilities from '@/scripts/admin/stub/abilities'
import BaseFormatMoney from '@/scripts/components/base/BaseFormatMoney.vue'
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 companyStore = useCompanyStore()
const { t } = useI18n()
const textSearch = ref(null)
const formatMoney = (amount) => {
return companyStore.formatMoney(amount)
}
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

@@ -1,68 +0,0 @@
<template>
<div>
<label class="flex text-heading 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-surface-muted"
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: '',
},
isMarkAsDefault: {
type: Boolean,
default: false,
},
})
const modalStore = useModalStore()
const { t } = useI18n()
function openTemplateModal() {
let markAsDefaultDescription = ''
if (props.storeProp == 'newEstimate') {
markAsDefaultDescription = t(
'estimates.mark_as_default_estimate_template_description'
)
} else if (props.storeProp == 'newInvoice') {
markAsDefaultDescription = t(
'invoices.mark_as_default_invoice_template_description'
)
}
modalStore.openModal({
title: t('general.choose_template'),
componentName: 'SelectTemplate',
data: {
templates: props.store.templates,
store: props.store,
storeProp: props.storeProp,
isMarkAsDefault: props.isMarkAsDefault,
markAsDefaultDescription,
},
})
}
</script>

View File

@@ -1,181 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="onCancel" @open="loadData">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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="name"
object
/>
</BaseInputGroup>
</BaseInputGrid>
</div>
<div
class="z-0 flex justify-end p-4 border-t border-line-default 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="ArrowDownOnSquareIcon"
: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

@@ -1,161 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeCategoryModal">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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-line-default border-solid
"
>
<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="ArrowDownOnSquareIcon"
: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

@@ -1,260 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeCompanyModal" @open="getInitials">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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="$t('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="$t('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="$t('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="$t('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-surface-secondary border-t border-line-default">
<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="ArrowDownOnSquareIcon"
: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

@@ -1,663 +0,0 @@
<template>
<BaseModal
:show="modalActive"
@close="closeCustomerModal"
@open="setInitialData"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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="$t('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>
<BaseInputGroup :label="$t('customers.tax_id')">
<BaseInput
v-model="customerStore.currentCustomer.tax_id"
type="text"
class="mt-1 md:mt-0"
/>
</BaseInputGroup>
</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-muted">
{{ $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
:name="isShowPassword ? 'EyeIcon' : 'EyeSlashIcon'"
class="mr-1 text-muted 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="$t('customers.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
:name="isShowConfirmPassword ? 'EyeIcon' : 'EyeSlashIcon'"
class="mr-1 text-muted 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-line-default 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="ArrowDownOnSquareIcon"
: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() {
v$.value.$touch()
if (v$.value.$invalid && customerStore.currentCustomer.email === '') {
notificationStore.showNotification({
type: 'error',
message: t('settings.notification.please_enter_email'),
})
}
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

@@ -1,157 +0,0 @@
<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-muted"
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-surface-secondary border-t border-line-default">
<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

@@ -1,24 +0,0 @@
<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

@@ -1,484 +0,0 @@
<template>
<BaseModal
:show="modalActive"
@close="closeExchangeRateModal"
@open="fetchInitialData"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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="$t('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"
track-by="key"
@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"
track-by="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"
open-direction="top"
@input="v$.currentExchangeRate.currencies.$touch()"
/>
</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-line-default 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="ArrowDownOnSquareIcon"
: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

@@ -1,155 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeDiskModal" @open="loadData">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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-line-default"
>
<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="ArrowDownOnSquareIcon"
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 S3compat from '@/scripts/admin/components/modal-components/disks/S3CompatDisk.vue'
import DoSpaces from '@/scripts/admin/components/modal-components/disks/DoSpacesDisk.vue'
export default {
components: {
Dropbox,
Local,
S3,
S3compat,
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) {
try {
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)
modalStore.refreshData()
closeDiskModal()
} catch (e) {
// error is handled by the disk store
} finally {
isLoading.value = false
}
}
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

@@ -1,132 +0,0 @@
<template>
<BaseModal :show="show" @close="$emit('close')">
<template #header>
<div class="flex justify-between w-full">
{{ $t('members.invite_member') }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted cursor-pointer"
@click="$emit('close')"
/>
</div>
</template>
<form @submit.prevent="submitInvitation">
<div class="p-4 space-y-4">
<BaseInputGroup
:label="$t('members.email')"
:error="v$.email.$error && v$.email.$errors[0].$message"
required
>
<BaseInput
v-model="form.email"
type="email"
:invalid="v$.email.$error"
@input="v$.email.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('members.role')"
:error="v$.role_id.$error && v$.role_id.$errors[0].$message"
required
>
<BaseMultiselect
v-model="form.role_id"
:options="roles"
label="title"
value-prop="id"
track-by="title"
:searchable="true"
/>
</BaseInputGroup>
</div>
<div class="flex justify-end p-4 border-t border-line-default">
<BaseButton
variant="primary-outline"
class="mr-3"
@click="$emit('close')"
>
{{ $t('general.cancel') }}
</BaseButton>
<BaseButton
:loading="isSending"
:disabled="isSending"
type="submit"
>
{{ $t('members.invite_member') }}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useMembersStore } from '@/scripts/admin/stores/members'
import { useI18n } from 'vue-i18n'
import { helpers, required, email } from '@vuelidate/validators'
import { useVuelidate } from '@vuelidate/core'
import http from '@/scripts/http'
const props = defineProps({
show: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['close'])
const membersStore = useMembersStore()
const { t } = useI18n()
const isSending = ref(false)
const roles = ref([])
const form = reactive({
email: '',
role_id: null,
})
const rules = computed(() => ({
email: {
required: helpers.withMessage(t('validation.required'), required),
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
role_id: {
required: helpers.withMessage(t('validation.required'), required),
},
}))
const v$ = useVuelidate(
rules,
computed(() => form)
)
onMounted(async () => {
const response = await http.get('/api/v1/roles')
roles.value = response.data.data
})
async function submitInvitation() {
v$.value.$touch()
if (v$.value.$invalid) return
isSending.value = true
try {
await membersStore.inviteMember({
email: form.email,
role_id: form.role_id,
})
form.email = ''
form.role_id = null
v$.value.$reset()
emit('close')
} catch (e) {
// Error handled by store
} finally {
isSending.value = false
}
}
</script>

View File

@@ -1,275 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeItemModal">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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="name"
/>
</BaseInputGroup>
<BaseInputGroup
v-if="isTaxPerItemEnabled"
:label="$t('items.taxes')"
>
<BaseMultiselect
v-model="taxes"
:options="getTaxTypes"
mode="tags"
label="tax_name"
value-prop="id"
class="w-full"
:can-deselect="false"
:can-clear="false"
searchable
track-by="tax_name"
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-line-default 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="ArrowDownOnSquareIcon" :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.calculation_type === 'fixed' ? tax.fixed_amount : tax.percent) + (tax.calculation_type === 'fixed' ? companyStore.selectedCompanyCurrency.symbol : '%') + ')',
}
}
}),
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) => {
const amount = tax.calculation_type === 'fixed'
? new Intl.NumberFormat(undefined, {
style: 'currency',
currency: companyStore.selectedCompanyCurrency.code
}).format(tax.fixed_amount / 100)
: `${tax.percent}%`
return {
...tax,
tax_name: `${tax.name} (${amount})`
}
})
})
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: tax.calculation_type === 'fixed' ? tax.fixed_amount : Math.round(price.value * tax.percent),
percent: tax.percent,
fixed_amount: tax.fixed_amount,
calculation_type: tax.calculation_type,
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

@@ -1,143 +0,0 @@
<template>
<BaseModal
:show="modalStore.active && modalStore.componentName === 'ItemUnitModal'"
@close="closeItemUnitModal"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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-line-default border-solid
"
>
<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="ArrowDownOnSquareIcon"
: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: 2 }),
minLength(2)
),
},
}
})
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

@@ -1,182 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeTestModal">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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-line-default 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'
import { useCompanyMailStore } from '@/scripts/admin/stores/company-mail'
const props = defineProps({
storeType: {
type: String,
default: 'global',
},
})
let isSaving = ref(false)
let formData = reactive({
to: '',
subject: '',
message: '',
})
const modalStore = useModalStore()
const mailDriverStore = useMailDriverStore()
const companyMailStore = useCompanyMailStore()
const activeStore = computed(() => {
return props.storeType === 'company' ? companyMailStore : mailDriverStore
})
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 activeStore.value.sendTestMail(formData)
if (response.data) {
closeTestModal()
isSaving.value = false
}
}
function closeTestModal() {
modalStore.closeModal()
setTimeout(() => {
modalStore.resetModalData()
resetFormData()
}, 300)
}
</script>

View File

@@ -1,290 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeNoteModal" @open="setFields">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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="value"
class="mt-2"
/>
</BaseInputGroup>
<BaseSwitchSection
v-model="noteStore.currentNote.is_default"
:title="$t('settings.customization.notes.is_default')"
:description="$t('settings.customization.notes.is_default_description')"
_ />
<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-line-default
"
>
<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="ArrowDownOnSquareIcon" :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([
{label: t('settings.customization.notes.types.invoice'), value: 'Invoice'},
{label: t('settings.customization.notes.types.estimate'), value: 'Estimate'},
{label: t('settings.customization.notes.types.payment'), value: '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' || route.name === 'recurring-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>
.note-modal {
.header-editior .editor-menu-bar {
margin-left: 0.5px;
margin-right: 0px;
}
}
</style>

View File

@@ -1,133 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closePaymentModeModal">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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-line-default 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="ArrowDownOnSquareIcon" :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

@@ -1,299 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeRolesModal">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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-heading
px-4
md:px-8
py-1.5
"
>
{{ $t('settings.roles.permission', 2) }}
<span class="text-sm text-red-500"> *</span>
</h6>
<div
class="
text-sm
not-italic
font-medium
text-subtle
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-line-default 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-muted border-b border-line-default 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-line-default
"
>
<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="ArrowDownOnSquareIcon" :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

@@ -1,147 +0,0 @@
<template>
<BaseModal :show="modalActive" @close="closeModal" @open="setData">
<template #header>
<div class="flex justify-between w-full">
{{ modalTitle }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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-line-default border-solid
cursor-pointer
hover:border-primary-300
"
@click="selectedTemplate = template.name"
>
<img
:src="template.path"
:alt="template.name"
class="w-full min-h-[100px]"
/>
<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-surface-muted text-sm text-center absolute bottom-0 left-0',
{
'text-primary-500 bg-primary-100':
selectedTemplate === template.name,
'text-body': selectedTemplate != template.name,
},
]"
>
{{ template.name }}
</span>
</div>
</div>
<div v-if="!modalStore.data.store.isEdit" class="z-0 flex ml-3 pt-5">
<BaseCheckbox
v-model="modalStore.data.isMarkAsDefault"
:set-initial-value="false"
variant="primary"
:label="$t('general.mark_as_default')"
:description="modalStore.data.markAsDefaultDescription"
/>
</div>
</div>
<div class="z-0 flex justify-end p-4 border-t border-line-default 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="ArrowDownOnSquareIcon" :class="slotProps.class" />
</template>
{{ $t('general.choose') }}
</BaseButton>
</div>
</BaseModal>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useModalStore } from '@/scripts/stores/modal'
import { useUserStore } from '@/scripts/admin/stores/user'
const modalStore = useModalStore()
const userStore = useUserStore()
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)
// update default estimate or invoice template
if (!modalStore.data.store.isEdit && modalStore.data.isMarkAsDefault) {
if (modalStore.data.storeProp == 'newEstimate') {
await userStore.updateUserSettings({
settings: {
default_estimate_template: selectedTemplate.value,
},
})
} else if (modalStore.data.storeProp == 'newInvoice') {
await userStore.updateUserSettings({
settings: {
default_invoice_template: selectedTemplate.value,
},
})
}
}
closeModal()
}
function getTickImage() {
const imgUrl = new URL('$images/tick.png', import.meta.url)
return imgUrl
}
function closeModal() {
modalStore.closeModal()
setTimeout(() => {
modalStore.$reset()
}, 300)
}
</script>

View File

@@ -1,309 +0,0 @@
<template>
<BaseModal
:show="modalActive"
@close="closeSendEstimateModal"
@open="setInitialData"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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.cc')"
:error="v$.cc && v$.cc.$error && v$.cc.$errors[0].$message"
>
<BaseInput
v-model="estimateMailForm.cc"
type="email"
:invalid="v$.cc && v$.cc.$error"
@input="v$.cc && v$.cc.$touch()"
placeholder="Optional: CC recipient"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.bcc')"
:error="v$.bcc && v$.bcc.$error && v$.bcc.$errors[0].$message"
>
<BaseInput
v-model="estimateMailForm.bcc"
type="email"
:invalid="v$.bcc && v$.bcc.$error"
@input="v$.bcc && v$.bcc.$touch()"
placeholder="Optional: BCC recipient"
/>
</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-line-default 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="PhotoIcon" class="h-5 mr-2" />
{{ $t('general.preview') }}
</BaseButton>
</div>
</form>
<div v-else>
<div class="my-6 mx-4 border border-line-default relative">
<BaseButton
class="absolute top-4 right-4"
:disabled="isLoading"
variant="primary-outline"
@click="cancelPreview"
>
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
{{ $t('general.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-line-default 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,
cc: null,
bcc: null,
subject: t('estimates.new_estimate'),
body: null,
})
const emit = defineEmits(['update'])
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),
},
cc: {
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
bcc: {
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) {
emit('update')
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

@@ -1,322 +0,0 @@
<template>
<BaseModal
:show="modalActive"
@close="closeSendInvoiceModal"
@open="setInitialData"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalTitle }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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
:label="$t('general.cc')"
:error="v$.cc && v$.cc.$error && v$.cc.$errors[0].$message"
>
<BaseInput
v-model="invoiceMailForm.cc"
type="text"
:invalid="v$.cc && v$.cc.$error"
@input="v$.cc && v$.cc.$touch()"
placeholder="Optional: CC recipient"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.bcc')"
:error="v$.bcc && v$.bcc.$error && v$.bcc.$errors[0].$message"
>
<BaseInput
v-model="invoiceMailForm.bcc"
type="text"
:invalid="v$.bcc && v$.bcc.$error"
@input="v$.bcc && v$.bcc.$touch()"
placeholder="Optional: BCC recipient"
/>
</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-line-default 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="PhotoIcon"
/>
</template>
{{ $t('general.preview') }}
</BaseButton>
</div>
</form>
<div v-else>
<div class="my-6 mx-4 border border-line-default relative">
<BaseButton
class="absolute top-4 right-4"
:disabled="isLoading"
variant="primary-outline"
@click="cancelPreview"
>
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
{{ $t('general.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-line-default 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 emit = defineEmits(['update'])
const invoiceMailFields = ref([
'customer',
'customerCustom',
'invoice',
'invoiceCustom',
'company',
])
const invoiceMailForm = reactive({
id: null,
from: null,
to: null,
cc: null,
bcc: null,
subject: t('invoices.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),
},
cc: {
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
bcc: {
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)
isLoading.value = false
if (response.data.success) {
emit('update', modalStore.id)
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

@@ -1,315 +0,0 @@
<template>
<BaseModal
:show="modalActive"
@close="closeSendPaymentModal"
@open="setInitialData"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalTitle }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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
:label="$t('general.cc')"
:error="v$.cc && v$.cc.$error && v$.cc.$errors[0].$message"
>
<BaseInput
v-model="paymentMailForm.cc"
type="email"
:invalid="v$.cc && v$.cc.$error"
@input="v$.cc && v$.cc.$touch()"
placeholder="Optional: CC recipient"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('general.bcc')"
:error="v$.bcc && v$.bcc.$error && v$.bcc.$errors[0].$message"
>
<BaseInput
v-model="paymentMailForm.bcc"
type="email"
:invalid="v$.bcc && v$.bcc.$error"
@input="v$.bcc && v$.bcc.$touch()"
placeholder="Optional: BCC recipient"
/>
</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-line-default 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="PhotoIcon"
/>
</template>
{{ $t('general.preview') }}
</BaseButton>
</div>
</form>
<div v-else>
<div class="my-6 mx-4 border border-line-default relative">
<BaseButton
class="absolute top-4 right-4"
:disabled="isLoading"
variant="primary-outline"
@click="cancelPreview"
>
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
{{ $t('general.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-line-default 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,
cc: null,
bcc: null,
subject: t('payments.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),
},
cc: {
email: helpers.withMessage(t('validation.email_incorrect'), email),
},
bcc: {
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)
isLoading.value = false
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

@@ -1,285 +0,0 @@
<template>
<BaseModal
:show="modalStore.active && modalStore.componentName === 'TaxTypeModal'"
@close="closeTaxTypeModal"
>
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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.tax_type')"
variant="horizontal"
required
>
<BaseSelectInput
v-model="taxTypeStore.currentTaxType.calculation_type"
:options="[
{ id: 'percentage', label: $t('tax_types.percentage') },
{ id: 'fixed', label: $t('tax_types.fixed_amount') }
]"
:allow-empty="false"
value-prop="id"
label-prop="label"
track-by="label"
:searchable="false"
/>
</BaseInputGroup>
<BaseInputGroup
v-if="taxTypeStore.currentTaxType.calculation_type === 'percentage'"
:label="$t('tax_types.percent')"
variant="horizontal"
required
>
<BaseMoney
v-model="taxTypeStore.currentTaxType.percent"
:currency="{
decimal: '.',
thousands: ',',
symbol: '% ',
precision: 2,
masked: false,
}"
/>
</BaseInputGroup>
<BaseInputGroup
v-else
:label="$t('tax_types.fixed_amount')"
variant="horizontal"
required
>
<BaseMoney
v-model="fixedAmount"
:currency="defaultCurrency"
/>
</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>
</BaseInputGrid>
</div>
<div
class="
z-0
flex
justify-end
p-4
border-t border-solid border-line-default
"
>
<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="ArrowDownOnSquareIcon"
: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 { useCompanyStore } from '@/scripts/admin/stores/company'
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 companyStore = useCompanyStore()
const defaultCurrency = computed(() => companyStore.selectedCompanyCurrency)
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)
),
},
calculation_type: {
required: helpers.withMessage(t('validation.required'), required),
},
percent: {
required: helpers.withMessage(t('validation.required'), required),
between: helpers.withMessage(
t('validation.enter_valid_tax_rate'),
between(-100, 100)
),
},
fixed_amount: {
required: helpers.withMessage(t('validation.required'), required),
},
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 (estimateStore.getSubtotalWithDiscount) {
if (taxData.calculation_type === 'percentage') {
amount = Math.round(
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
)
} else {
amount = taxData.fixed_amount
}
}
let data = {
...TaxStub,
id: Guid.raw(),
name: taxData.name,
calculation_type: taxData.calculation_type,
percent: taxData.percent,
fixed_amount: taxData.fixed_amount,
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,
calculation_type: taxData.calculation_type,
percent: taxData.percent,
fixed_amount: taxData.fixed_amount,
tax_type_id: taxData.id,
}
modalStore.refreshData(data)
}
}
function closeTaxTypeModal() {
modalStore.closeModal()
setTimeout(() => {
taxTypeStore.resetCurrentTaxType()
v$.value.$reset()
}, 300)
}
const fixedAmount = computed({
get: () => taxTypeStore.currentTaxType.fixed_amount / 100,
set: (value) => {
taxTypeStore.currentTaxType.fixed_amount = Math.round(value * 100)
},
})
</script>

View File

@@ -1,209 +0,0 @@
<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-muted mt-1">
{{ modalStore.content }}
</p>
</div>
<BaseIcon
name="XMarkIcon"
class="h-6 w-6 text-muted 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-line-default 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="ArrowDownOnSquareIcon"
:class="slotProps.class"
/>
</template>
{{ $t('general.save') }}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
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

@@ -1,421 +0,0 @@
<template>
<BaseModal :show="modalActive" @open="setData">
<template #header>
<div class="flex justify-between w-full">
{{ modalStore.title }}
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-muted 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"
value-prop="value"
: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-subtle'
: '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-line-default
"
>
<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="ArrowDownOnSquareIcon"
/>
</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([
{label: t('settings.custom_fields.model_type.customer'), value: 'Customer'},
{label: t('settings.custom_fields.model_type.invoice'), value: 'Invoice'},
{label: t('settings.custom_fields.model_type.estimate'), value: 'Estimate'},
{label: t('settings.custom_fields.model_type.expense'), value: 'Expense'},
{label: t('settings.custom_fields.model_type.payment'), value: '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: {
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

@@ -1,36 +0,0 @@
<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

@@ -1,330 +0,0 @@
<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="$t('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"
track-by="name"
@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-heading 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

@@ -1,299 +0,0 @@
<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-heading 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

@@ -1,221 +0,0 @@
<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="$t('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-heading 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

@@ -1,325 +0,0 @@
<template>
<form @submit.prevent="submitData">
<div class="px-8 py-6">
<BaseInputGrid>
<BaseInputGroup
:label="$t('settings.disk.name')"
:error="
v$.s3CompatDiskConfigData.name.$error &&
v$.s3CompatDiskConfigData.name.$errors[0].$message
"
required
>
<BaseInput
v-model="diskStore.s3CompatDiskConfigData.name"
type="text"
name="name"
:invalid="v$.s3CompatDiskConfigData.name.$error"
@input="v$.s3CompatDiskConfigData.name.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.driver')"
:error="
v$.s3CompatDiskConfigData.selected_driver.$error &&
v$.s3CompatDiskConfigData.selected_driver.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="selected_driver"
:invalid="v$.s3CompatDiskConfigData.selected_driver.$error"
value-prop="value"
:options="disks"
searchable
label="name"
:can-deselect="false"
@update:modelValue="onChangeDriver(data)"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_endpoint')"
:error="
v$.s3CompatDiskConfigData.root.$error &&
v$.s3CompatDiskConfigData.root.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.endpoint"
type="url"
name="endpoint"
placeholder="http://127.0.0.1:9005"
:invalid="v$.s3CompatDiskConfigData.endpoint.$error"
@input="v$.s3CompatDiskConfigData.endpoint.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_root')"
:error="
v$.s3CompatDiskConfigData.root.$error &&
v$.s3CompatDiskConfigData.root.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.root"
type="text"
name="root"
placeholder="Ex. /user/root/"
:invalid="v$.s3CompatDiskConfigData.root.$error"
@input="v$.s3CompatDiskConfigData.root.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_key')"
:error="
v$.s3CompatDiskConfigData.key.$error &&
v$.s3CompatDiskConfigData.key.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.key"
type="text"
name="key"
placeholder="Ex. KEIS4S39SERSDS"
:invalid="v$.s3CompatDiskConfigData.key.$error"
@input="v$.s3CompatDiskConfigData.key.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_secret')"
:error="
v$.s3CompatDiskConfigData.secret.$error &&
v$.s3CompatDiskConfigData.secret.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.secret"
type="text"
name="secret"
placeholder="Ex. ********"
:invalid="v$.s3CompatDiskConfigData.secret.$error"
@input="v$.s3CompatDiskConfigData.secret.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_region')"
:error="
v$.s3CompatDiskConfigData.region.$error &&
v$.s3CompatDiskConfigData.region.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.region"
type="text"
name="region"
placeholder="Ex. us-west"
:invalid="v$.s3CompatDiskConfigData.region.$error"
@input="v$.s3CompatDiskConfigData.region.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('settings.disk.s3_bucket')"
:error="
v$.s3CompatDiskConfigData.bucket.$error &&
v$.s3CompatDiskConfigData.bucket.$errors[0].$message
"
required
>
<BaseInput
v-model.trim="diskStore.s3CompatDiskConfigData.bucket"
type="text"
name="bucket"
placeholder="Ex. AppName"
:invalid="v$.s3CompatDiskConfigData.bucket.$error"
@input="v$.s3CompatDiskConfigData.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-heading 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.s3CompatDiskConfigData.selected_driver = value
},
})
const rules = computed(() => {
return {
s3CompatDiskConfigData: {
name: {
required: helpers.withMessage(t('validation.required'), required),
},
endpoint: {
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.s3CompatDiskConfigData = {
name: null,
selected_driver: 's3compat',
key: null,
secret: null,
region: null,
bucket: null,
root: null,
}
})
loadData()
async function loadData() {
isLoading.value = true
let data = reactive({
disk: 's3compat',
})
if (props.isEdit) {
Object.assign(diskStore.s3CompatDiskConfigData, 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.s3CompatDiskConfigData, diskData.data)
}
selected_disk.value = props.disks.find((v) => v.value == 's3compat')
isLoading.value = false
}
const isDisabled = computed(() => {
return props.isEdit && set_as_default.value && is_current_disk.value
? true
: false
})
async function submitData() {
v$.value.s3CompatDiskConfigData.$touch()
if (v$.value.s3CompatDiskConfigData.$invalid) {
return true
}
let data = {
credentials: diskStore.s3CompatDiskConfigData,
name: diskStore.s3CompatDiskConfigData.name,
driver: selected_disk.value.value,
set_as_default: set_as_default.value,
}
emit('submit', data)
return false
}
function onChangeDriver() {
emit('onChangeDisk', diskStore.s3CompatDiskConfigData.selected_driver)
}
return {
v$,
diskStore,
modalStore,
set_as_default,
isLoading,
selected_disk,
selected_driver,
is_current_disk,
loadData,
submitData,
onChangeDriver,
isDisabled,
}
},
}
</script>

View File

@@ -1,304 +0,0 @@
<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="$t('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-heading 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>

View File

@@ -1,105 +0,0 @@
<template>
<div v-if="isAppLoaded" class="h-full">
<NotificationRoot />
<ImpersonationBanner />
<SiteHeader />
<SiteSidebar v-if="hasCompany" />
<ExchangeRateBulkUpdateModal />
<main
:class="[
'h-screen h-screen-ios overflow-y-auto min-h-0 transition-all duration-300',
hasCompany ? (globalStore.isSidebarCollapsed ? 'md:pl-16' : 'md:pl-56 xl:pl-64') : '',
]"
>
<div class="pt-16 pb-16">
<router-view />
</div>
</main>
</div>
<BaseGlobalLoader v-else />
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useModalStore } from '@/scripts/stores/modal'
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import SiteHeader from '@/scripts/admin/layouts/partials/TheSiteHeader.vue'
import SiteSidebar from '@/scripts/admin/layouts/partials/TheSiteSidebar.vue'
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
import ExchangeRateBulkUpdateModal from '@/scripts/admin/components/modal-components/ExchangeRateBulkUpdateModal.vue'
import ImpersonationBanner from '@/scripts/admin/components/ImpersonationBanner.vue'
const globalStore = useGlobalStore()
const route = useRoute()
const userStore = useUserStore()
const router = useRouter()
const modalStore = useModalStore()
const { t } = useI18n()
const exchangeRateStore = useExchangeRateStore()
const companyStore = useCompanyStore()
const isAppLoaded = computed(() => {
return globalStore.isAppLoaded
})
const hasCompany = computed(() => {
return !!companyStore.selectedCompany || companyStore.isAdminMode
})
onMounted(() => {
globalStore.bootstrap().then((res) => {
if (companyStore.isAdminMode) {
return
}
if (!res.data.current_company) {
if (route.name !== 'no.company') {
router.push({ name: 'no.company' })
}
return
}
if (route.meta.ability && !userStore.hasAbilities(route.meta.ability)) {
router.push({ name: 'account.settings' })
} else if (route.meta.isSuperAdmin && !userStore.currentUser.is_super_admin) {
router.push({ name: 'dashboard' })
} else if (route.meta.isOwner && !userStore.currentUser.is_owner) {
router.push({ name: 'account.settings' })
}
if (
res.data.current_company_settings.bulk_exchange_rate_configured === 'NO'
) {
exchangeRateStore.fetchBulkCurrencies().then((res) => {
if (res.data.currencies.length) {
modalStore.openModal({
componentName: 'ExchangeRateBulkUpdateModal',
size: 'sm',
})
} else {
let data = {
settings: {
bulk_exchange_rate_configured: 'YES',
},
}
companyStore.updateCompanySettings({
data,
})
}
})
}
})
})
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div class="h-screen overflow-y-auto text-base">
<NotificationRoot />
<div class="container mx-auto px-4">
<router-view />
</div>
</div>
</template>
<script setup>
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
</script>

View File

@@ -1,177 +0,0 @@
<template>
<div class="grid h-screen grid-cols-12 overflow-y-hidden bg-surface-tertiary">
<NotificationRoot />
<div
class="
flex
items-center
justify-center
w-full
max-w-sm
col-span-12
p-4
mx-auto
text-heading
md:p-8 md:col-span-6
lg:col-span-4
flex-2
md:pb-48 md:pt-40
"
>
<div class="w-full">
<MainLogo
v-if="!loginPageLogo"
class="block w-48 h-auto max-w-full mb-32 text-primary-500"
/>
<img
v-else
:src="loginPageLogo"
class="block w-48 h-auto max-w-full mb-32 text-primary-500"
/>
<router-view />
<div
class="
pt-24
mt-0
text-sm
not-italic
font-medium
leading-relaxed
text-left text-subtle
md:pt-40
"
>
<p v-if="copyrightText" class="mb-3">
{{ copyrightText }}
</p>
<p v-else class="mb-3">
Powered by
<a
href="https://invoiceshelf.com/"
target="_blank"
rel="noopener noreferrer"
class="text-primary-500 hover:underline"
>InvoiceShelf</a>
</p>
</div>
</div>
</div>
<div
class="
relative
flex-col
items-center
justify-center
hidden
w-full
h-full
pl-10
bg-no-repeat bg-cover
md:col-span-6
lg:col-span-8
md:flex
content-box
overflow-hidden
"
>
<LoginBackground class="absolute h-full w-full" />
<LoginPlanetCrater
class="absolute z-10 top-0 right-0 h-[300px] w-[420px]"
/>
<LoginBackgroundOverlay class="absolute h-full w-full right-[7.5%]" />
<div class="md:pl-10 xl:pl-0 relative z-50 w-7/12 xl:w-5/12 xl:w-5/12">
<h1
class="
hidden
mb-3
text-3xl
leading-normal
text-left text-white
xl:text-5xl xl:leading-tight
md:none
lg:block
"
>
{{ pageHeading }}
</h1>
<p
class="
hidden
text-sm
not-italic
font-normal
leading-normal
text-left text-gray-100
xl:text-base xl:leading-6
md:none
lg:block
"
>
{{ pageDescription }}
</p>
</div>
<LoginBottomVector
class="
absolute
z-50
w-full
bg-no-repeat
content-bottom
h-[15vw]
lg:h-[22vw]
right-[32%]
bottom-0
"
/>
</div>
</div>
</template>
<script setup>
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
import LoginBackground from '@/scripts/components/svg/LoginBackground.vue'
import LoginPlanetCrater from '@/scripts/components/svg/LoginPlanetCrater.vue'
import LoginBottomVector from '@/scripts/components/svg/LoginBottomVector.vue'
import LoginBackgroundOverlay from '@/scripts/components/svg/LoginBackgroundOverlay.vue'
import { computed, ref } from 'vue'
const pageHeading = computed(() => {
if (window.login_page_heading) {
return window.login_page_heading
}
return 'Simple Invoicing for Individuals Small Businesses'
})
const pageDescription = computed(() => {
if (window.login_page_description) {
return window.login_page_description
}
return 'InvoiceShelf helps you track expenses, record payments & generate beautiful invoices & estimates.'
})
const copyrightText = computed(() => {
return window.copyright_text || null
})
const loginPageLogo = computed(() => {
if (window.login_page_logo) {
return window.login_page_logo
}
return false
})
</script>
<style scoped>
</style>

View File

@@ -1,282 +0,0 @@
<template>
<header
class="
fixed
top-0
left-0
z-20
flex
items-center
justify-between
w-full
px-4
py-3
md:h-16 md:px-8
bg-linear-to-r
from-header-from
to-header-to
"
>
<div class="flex items-center">
<router-link
:to="companyStore.isAdminMode ? '/admin/administration/dashboard' : '/admin/dashboard'"
class="
text-lg
not-italic
font-black
tracking-wider
text-white
brand-main
font-base
hidden
md:block
"
>
<img v-if="adminLogo" :src="adminLogo" class="h-6" />
<MainLogo v-else class="h-6" light-color="white" dark-color="white" />
</router-link>
</div>
<!-- Mobile toggle button-->
<div
:class="{ 'is-active': globalStore.isSidebarOpen }"
class="
flex
float-left
p-1
overflow-visible
text-sm
ease-linear
bg-surface
border-0
rounded
cursor-pointer
md:hidden md:ml-0
hover:bg-hover-strong
"
@click.prevent="onToggle"
>
<BaseIcon name="Bars3Icon" class="!w-6 !h-6 text-muted" />
</div>
<ul class="flex float-right h-8 m-0 list-none md:h-9">
<li
v-if="hasCreateAbilities() && !companyStore.isAdminMode"
class="relative hidden float-left m-0 md:block"
>
<BaseDropdown width-class="w-48">
<template #activator>
<div
class="
flex
items-center
justify-center
w-8
h-8
ml-2
text-sm text-white
bg-white/20
rounded-lg
hover:bg-white/30
md:h-9 md:w-9
"
>
<BaseIcon name="PlusIcon" class="w-5 h-5 text-white" />
</div>
</template>
<router-link to="/admin/invoices/create">
<BaseDropdownItem
v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)"
>
<BaseIcon
name="DocumentTextIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
aria-hidden="true"
/>
{{ $t('invoices.new_invoice') }}
</BaseDropdownItem>
</router-link>
<router-link to="/admin/estimates/create">
<BaseDropdownItem
v-if="userStore.hasAbilities(abilities.CREATE_ESTIMATE)"
>
<BaseIcon
name="DocumentIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
aria-hidden="true"
/>
{{ $t('estimates.new_estimate') }}
</BaseDropdownItem>
</router-link>
<router-link to="/admin/customers/create">
<BaseDropdownItem
v-if="userStore.hasAbilities(abilities.CREATE_CUSTOMER)"
>
<BaseIcon
name="UserIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
aria-hidden="true"
/>
{{ $t('customers.new_customer') }}
</BaseDropdownItem>
</router-link>
</BaseDropdown>
</li>
<li v-if="!companyStore.isAdminMode" class="ml-2">
<GlobalSearchBar
v-if="
userStore.currentUser.is_owner ||
userStore.hasAbilities(abilities.VIEW_CUSTOMER)
"
/>
</li>
<li>
<CompanySwitcher />
</li>
<!-- User Dropdown-->
<li class="relative block float-left ml-2">
<BaseDropdown width-class="w-48">
<template #activator>
<img
:src="previewAvatar"
class="block w-8 h-8 rounded-full ring-2 ring-white/30 md:h-9 md:w-9 object-cover"
/>
</template>
<!-- Theme Toggle -->
<div class="px-3 py-2">
<div class="flex items-center justify-between rounded-lg bg-surface-secondary p-1">
<button
v-for="opt in themeOptions"
:key="opt.value"
:class="[
'flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors',
currentTheme === opt.value
? 'bg-surface text-heading shadow-sm'
: 'text-muted hover:text-body',
]"
@click.stop="setTheme(opt.value)"
>
<BaseIcon :name="opt.icon" class="w-3.5 h-3.5" />
</button>
</div>
</div>
<router-link to="/admin/user-settings">
<BaseDropdownItem>
<BaseIcon
name="CogIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
aria-hidden="true"
/>
{{ $t('navigation.settings') }}
</BaseDropdownItem>
</router-link>
<BaseDropdownItem @click="logout">
<BaseIcon
name="ArrowRightOnRectangleIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
aria-hidden="true"
/>
{{ $t('navigation.logout') }}
</BaseDropdownItem>
</BaseDropdown>
</li>
</ul>
</header>
</template>
<script setup>
import { useAuthStore } from '@/scripts/admin/stores/auth'
import { useRouter } from 'vue-router'
import { computed, ref, onMounted } from 'vue'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import CompanySwitcher from '@/scripts/components/CompanySwitcher.vue'
import GlobalSearchBar from '@/scripts/components/GlobalSearchBar.vue'
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
import abilities from '@/scripts/admin/stub/abilities'
const authStore = useAuthStore()
const userStore = useUserStore()
const globalStore = useGlobalStore()
const companyStore = useCompanyStore()
const router = useRouter()
const previewAvatar = computed(() => {
return userStore.currentUser && userStore.currentUser.avatar !== 0
? userStore.currentUser.avatar
: getDefaultAvatar()
})
const adminLogo = computed(() => {
if (globalStore.globalSettings.admin_portal_logo) {
return '/storage/' + globalStore.globalSettings.admin_portal_logo
}
return false
})
function getDefaultAvatar() {
const imgUrl = new URL('$images/default-avatar.jpg', import.meta.url)
return imgUrl
}
function hasCreateAbilities() {
return userStore.hasAbilities([
abilities.CREATE_INVOICE,
abilities.CREATE_ESTIMATE,
abilities.CREATE_CUSTOMER,
])
}
async function logout() {
await authStore.logout()
router.push('/login')
}
function onToggle() {
globalStore.setSidebarVisibility(true)
}
const themeOptions = [
{ value: 'light', icon: 'SunIcon' },
{ value: 'dark', icon: 'MoonIcon' },
{ value: 'system', icon: 'ComputerDesktopIcon' },
]
const currentTheme = ref(localStorage.getItem('theme') || 'system')
function applyTheme(theme) {
if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark')
} else {
document.documentElement.removeAttribute('data-theme')
}
}
function setTheme(theme) {
currentTheme.value = theme
localStorage.setItem('theme', theme)
applyTheme(theme)
}
onMounted(() => {
applyTheme(currentTheme.value)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (currentTheme.value === 'system') {
applyTheme('system')
}
})
})
</script>

View File

@@ -1,223 +0,0 @@
<template>
<!-- MOBILE MENU -->
<TransitionRoot as="template" :show="globalStore.isSidebarOpen">
<Dialog
as="div"
class="fixed inset-0 z-40 flex md:hidden"
@close="globalStore.setSidebarVisibility(false)"
>
<TransitionChild
as="template"
enter="transition-opacity ease-linear duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="transition-opacity ease-linear duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-gray-600/75" />
</TransitionChild>
<TransitionChild
as="template"
enter="transition ease-in-out duration-300"
enter-from="-translate-x-full"
enter-to="translate-x-0"
leave="transition ease-in-out duration-300"
leave-from="translate-x-0"
leave-to="-translate-x-full"
>
<div class="relative flex flex-col flex-1 w-full max-w-xs bg-surface">
<TransitionChild
as="template"
enter="ease-in-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in-out duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="absolute top-0 right-0 pt-2 -mr-12">
<button
class="
flex
items-center
justify-center
w-10
h-10
ml-1
rounded-full
focus:outline-hidden
focus:ring-2
focus:ring-inset
focus:ring-white
"
@click="globalStore.setSidebarVisibility(false)"
>
<span class="sr-only">Close sidebar</span>
<BaseIcon
name="XMarkIcon"
class="w-6 h-6 text-white"
aria-hidden="true"
/>
</button>
</div>
</TransitionChild>
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div class="flex items-center shrink-0 px-4 mb-10">
<MainLogo
class="block h-auto max-w-full w-36 text-primary-400"
alt="InvoiceShelf Logo"
/>
</div>
<nav
v-for="(menu, index) in globalStore.menuGroups"
:key="index"
class="mt-5 space-y-1"
>
<div
v-if="menu[0] && menu[0].group_label"
class="px-4 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider"
>
{{ $t(menu[0].group_label) }}
</div>
<router-link
v-for="item in menu"
:key="item.name"
:to="item.link"
:class="[
hasActiveUrl(item.link)
? 'text-primary-600 bg-primary-50 font-semibold'
: 'text-body hover:bg-hover',
'cursor-pointer mx-3 px-3 py-2.5 flex items-center rounded-lg text-sm not-italic font-medium transition-colors',
]"
@click="globalStore.setSidebarVisibility(false)"
>
<BaseIcon
:name="item.icon"
:class="[
hasActiveUrl(item.link)
? 'text-primary-500'
: 'text-subtle',
'mr-3 shrink-0 h-5 w-5',
]"
@click="globalStore.setSidebarVisibility(false)"
/>
{{ $t(item.title) }}
</router-link>
</nav>
</div>
</div>
</TransitionChild>
<div class="shrink-0 w-14">
<!-- Force sidebar to shrink to fit close icon -->
</div>
</Dialog>
</TransitionRoot>
<!-- DESKTOP MENU -->
<div
:class="[
globalStore.isSidebarCollapsed ? 'w-16' : 'w-56 xl:w-64',
]"
class="
hidden
h-screen
pb-0
overflow-y-auto overflow-x-hidden
bg-surface/80 backdrop-blur-xl
border-r border-white/10
md:fixed md:flex md:flex-col md:inset-y-0
pt-16
transition-all duration-300
"
>
<div
v-for="(menu, index) in globalStore.menuGroups"
:key="index"
class="p-0 m-0 mt-4 list-none"
>
<div
v-if="menu[0] && menu[0].group_label && !globalStore.isSidebarCollapsed"
class="px-6 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider whitespace-nowrap"
>
{{ $t(menu[0].group_label) }}
</div>
<div
v-else-if="menu[0] && menu[0].group_label && globalStore.isSidebarCollapsed"
class="mx-3 my-2 border-t border-line-light"
/>
<router-link
v-for="item in menu"
:key="item"
:to="item.link"
v-tooltip="globalStore.isSidebarCollapsed ? { content: $t(item.title), placement: 'right' } : null"
:class="[
hasActiveUrl(item.link)
? 'text-primary-600 bg-primary-50 font-semibold'
: 'text-body hover:bg-hover',
globalStore.isSidebarCollapsed
? 'cursor-pointer mx-2 px-0 py-2.5 group flex items-center justify-center rounded-lg text-sm font-medium transition-colors'
: 'cursor-pointer mx-3 px-3 py-2.5 group flex items-center rounded-lg text-sm not-italic font-medium transition-colors',
]"
>
<BaseIcon
:name="item.icon"
:class="[
hasActiveUrl(item.link)
? 'text-primary-500'
: 'text-subtle group-hover:text-body',
globalStore.isSidebarCollapsed
? 'shrink-0 h-6 w-6'
: 'mr-3 shrink-0 h-5 w-5',
]"
/>
<span v-if="!globalStore.isSidebarCollapsed" class="whitespace-nowrap">
{{ $t(item.title) }}
</span>
</router-link>
</div>
<!-- Bottom toolbar -->
<div class="mt-auto sticky bottom-0 border-t border-white/10 bg-surface/80 backdrop-blur-xl p-2 flex flex-col items-center gap-1">
<button
v-tooltip="globalStore.isSidebarCollapsed ? { content: $t('general.collapse'), placement: 'right' } : null"
:class="[
globalStore.isSidebarCollapsed
? 'w-10 h-10 justify-center'
: 'w-full px-3 h-10 justify-end',
]"
class="flex items-center rounded-lg text-subtle hover:text-body hover:bg-hover transition-colors"
@click="globalStore.toggleSidebarCollapse()"
>
<BaseIcon
:name="globalStore.isSidebarCollapsed ? 'ChevronDoubleRightIcon' : 'ChevronDoubleLeftIcon'"
class="w-4 h-4 shrink-0"
/>
</button>
</div>
</div>
</template>
<script setup>
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
import {
Dialog,
DialogOverlay,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
import { useRoute } from 'vue-router'
import { useGlobalStore } from '@/scripts/admin/stores/global'
const route = useRoute()
const globalStore = useGlobalStore()
function hasActiveUrl(url) {
return route.path.indexOf(url) > -1
}
</script>

View File

@@ -1,162 +0,0 @@
import http from '@/scripts/http'
import Ls from '@/scripts/services/ls.js'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useAdministrationStore = defineStore('administration', {
state: () => ({
companies: [],
totalCompanies: 0,
selectedCompany: null,
users: [],
totalUsers: 0,
}),
actions: {
fetchCompanies(params) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/super-admin/companies', { params })
.then((response) => {
this.companies = response.data.data
this.totalCompanies = response.data.meta
? response.data.meta.total
: response.data.data.length
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCompany(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/super-admin/companies/${id}`)
.then((response) => {
this.selectedCompany = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchUsers(params) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/super-admin/users', { params })
.then((response) => {
this.users = response.data.data
this.totalUsers = response.data.meta
? response.data.meta.total
: response.data.data.length
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchUser(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/super-admin/users/${id}`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateUser(id, data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/super-admin/users/${id}`, data)
.then((response) => {
const { global } = window.i18n
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('administration.users.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
impersonateUser(id) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/super-admin/users/${id}/impersonate`)
.then((response) => {
Ls.set('admin.impersonating', 'true')
Ls.set('auth.token', `Bearer ${response.data.token}`)
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
stopImpersonating() {
return new Promise((resolve, reject) => {
http
.post('/api/v1/super-admin/stop-impersonating')
.then((response) => {
Ls.remove('admin.impersonating')
Ls.remove('auth.token')
resolve(response)
})
.catch((err) => {
Ls.remove('admin.impersonating')
Ls.remove('auth.token')
handleError(err)
reject(err)
})
})
},
updateCompany(id, data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/super-admin/companies/${id}`, data)
.then((response) => {
const { global } = window.i18n
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t(
'administration.companies.updated_message'
),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})

View File

@@ -1,78 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useAuthStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('auth', {
state: () => ({
status: '',
loginData: {
email: '',
password: '',
remember: '',
},
}),
actions: {
login(data) {
return new Promise((resolve, reject) => {
http.get('/sanctum/csrf-cookie').then((response) => {
if (response) {
http
.post('/login', data)
.then((response) => {
resolve(response)
setTimeout(() => {
this.loginData.email = ''
this.loginData.password = ''
}, 1000)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
})
},
logout() {
return new Promise((resolve, reject) => {
http
.post('/auth/logout')
.then(async (response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: 'Logged out successfully.',
})
// Clear stored auth data so next login doesn't send stale tokens
window.Ls.remove('auth.token')
window.Ls.remove('selectedCompany')
// Refresh CSRF token so next login works cleanly
await http.get('/sanctum/csrf-cookie').catch(() => {})
window.router.push('/login')
resolve(response)
})
.catch((err) => {
handleError(err)
window.Ls.remove('auth.token')
window.Ls.remove('selectedCompany')
http.get('/sanctum/csrf-cookie').catch(() => {})
window.router.push('/login')
reject(err)
})
})
},
},
})()
}

View File

@@ -1,74 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useBackupStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('backup', {
state: () => ({
backups: [],
currentBackupData: {
option: 'full',
selected_disk: null,
},
}),
actions: {
fetchBackups(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/backups`, { params })
.then((response) => {
this.backups = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
createBackup(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/backups`, data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.backup.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
removeBackup(params) {
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/backups/${params.disk}`, { params })
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.backup.deleted_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,127 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useCategoryStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('category', {
state: () => ({
categories: [],
currentCategory: {
id: null,
name: '',
description: '',
},
editCategory: null
}),
getters: {
isEdit: (state) => (state.currentCategory.id ? true : false),
},
actions: {
fetchCategories(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/categories`, { params })
.then((response) => {
this.categories = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCategory(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/categories/${id}`)
.then((response) => {
this.currentCategory = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addCategory(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/categories', data)
.then((response) => {
this.categories.push(response.data.data)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.expense_category.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCategory(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/categories/${data.id}`, data)
.then((response) => {
if (response.data) {
let pos = this.categories.findIndex(
(category) => category.id === response.data.data.id
)
this.categories[pos] = data.categories
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t(
'settings.expense_category.updated_message'
),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteCategory(id) {
return new Promise((resolve) => {
http
.delete(`/api/v1/categories/${id}`)
.then((response) => {
let index = this.categories.findIndex(
(category) => category.id === id
)
this.categories.splice(index, 1)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.expense_category.deleted_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
console.error(err)
})
})
},
},
})()
}

View File

@@ -1,91 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useCompanyMailStore = defineStore('company-mail', {
state: () => ({
mailConfigData: null,
mail_driver: '',
mail_drivers: [],
}),
actions: {
fetchMailDrivers() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/mail/drivers')
.then((response) => {
if (response.data) {
this.mail_drivers = response.data
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchMailConfig() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/company/mail/company-config')
.then((response) => {
if (response.data) {
this.mailConfigData = response.data
this.mail_driver = response.data.mail_driver || ''
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateMailConfig(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/company/mail/company-config', data)
.then((response) => {
const { global } = window.i18n
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.mail.company_mail_config_updated'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
sendTestMail(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/company/mail/company-test', data)
.then((response) => {
const { global } = window.i18n
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('general.send_mail_successfully'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})

View File

@@ -1,208 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
import Ls from '@/scripts/services/ls'
export const useCompanyStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('company', {
state: () => ({
companies: [],
selectedCompany: null,
selectedCompanySettings: {},
selectedCompanyCurrency: null,
isAdminMode: window.Ls?.get('isAdminMode') === 'true',
}),
actions: {
setSelectedCompany(data) {
if (data) {
window.Ls.set('selectedCompany', data.id)
window.Ls.remove('isAdminMode')
this.isAdminMode = false
} else {
window.Ls.remove('selectedCompany')
}
this.selectedCompany = data
},
setAdminMode(enabled) {
this.isAdminMode = enabled
if (enabled) {
window.Ls.set('isAdminMode', 'true')
window.Ls.remove('selectedCompany')
this.selectedCompany = null
} else {
window.Ls.remove('isAdminMode')
}
},
fetchBasicMailConfig() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/company/mail/config')
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCompany(data) {
return new Promise((resolve, reject) => {
http
.put('/api/v1/company', data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.company_info.updated_message'),
})
this.selectedCompany = response.data.data
const companyIndex = this.companies.findIndex((company) => company.unique_hash === this.selectedCompany.unique_hash);
if (companyIndex !== -1) {
this.companies[companyIndex] = this.selectedCompany;
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCompanyLogo(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/company/upload-logo', data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addNewCompany(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/companies', data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('company_switcher.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCompany(params) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/current-company', params)
.then((response) => {
Object.assign(this.companyForm, response.data.data.address)
this.companyForm.name = response.data.data.name
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchUserCompanies() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/companies')
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCompanySettings(settings) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/company/settings', {
params: {
settings,
},
})
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCompanySettings({ data, message }) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/company/settings', data)
.then((response) => {
Object.assign(this.selectedCompanySettings, data.settings)
if (message) {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t(message),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteCompany(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/companies/delete`, data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setDefaultCurrency(data) {
this.defaultCurrency = data.currency
},
},
})()
}

View File

@@ -1,209 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import customFieldStub from '@/scripts/admin/stub/custom-field'
import utilities from '@/scripts/helpers/utilities'
import { util } from 'prettier'
import { handleError } from '@/scripts/helpers/error-handling'
export const useCustomFieldStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('custom-field', {
state: () => ({
customFields: [],
isRequestOngoing: false,
currentCustomField: {
...customFieldStub,
},
}),
getters: {
isEdit() {
return this.currentCustomField.id ? true : false
},
},
actions: {
resetCustomFields() {
this.customFields = []
},
resetCurrentCustomField() {
this.currentCustomField = {
...customFieldStub,
}
},
fetchCustomFields(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/custom-fields`, { params })
.then((response) => {
this.customFields = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchNoteCustomFields(params) {
return new Promise((resolve, reject) => {
if (this.isRequestOngoing) {
resolve({ requestOnGoing: true })
return true
}
this.isRequestOngoing = true
http
.get(`/api/v1/custom-fields`, { params })
.then((response) => {
this.customFields = response.data.data
this.isRequestOngoing = false
resolve(response)
})
.catch((err) => {
this.isRequestOngoing = false
handleError(err)
reject(err)
})
})
},
fetchCustomField(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/custom-fields/${id}`)
.then((response) => {
this.currentCustomField = response.data.data
if (
this.currentCustomField.options &&
this.currentCustomField.options.length
) {
this.currentCustomField.options =
this.currentCustomField.options.map((option) => {
return (option = { name: option })
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addCustomField(params) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/custom-fields`, params)
.then((response) => {
let data = {
...response.data.data,
}
if (data.options) {
data.options = data.options.map((option) => {
return { name: option ? option : '' }
})
}
this.customFields.push(data)
notificationStore.showNotification({
type: 'success',
message: global.t('settings.custom_fields.added_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCustomField(params) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.put(`/api/v1/custom-fields/${params.id}`, params)
.then((response) => {
let data = {
...response.data.data,
}
if (data.options) {
data.options = data.options.map((option) => {
return { name: option ? option : '' }
})
}
let pos = this.customFields.findIndex((_f) => _f.id === data.id)
if (this.customFields[pos]) {
this.customFields[pos] = data
}
notificationStore.showNotification({
type: 'success',
message: global.t('settings.custom_fields.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteCustomFields(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/custom-fields/${id}`)
.then((response) => {
let index = this.customFields.findIndex(
(field) => field.id === id
)
this.customFields.splice(index, 1)
if (response.data.error) {
notificationStore.showNotification({
type: 'error',
message: global.t('settings.custom_fields.already_in_use'),
})
} else {
notificationStore.showNotification({
type: 'success',
message: global.t('settings.custom_fields.deleted_message'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
// notificationStore.showNotification({
// type: 'error',
// message: global.t('settings.custom_fields.already_in_use'),
// })
reject(err)
})
})
},
},
})()
}

View File

@@ -1,256 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
import { handleError } from '@/scripts/helpers/error-handling'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import addressStub from '@/scripts/admin/stub/address.js'
import customerStub from '@/scripts/admin/stub/customer'
export const useCustomerStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('customer', {
state: () => ({
customers: [],
totalCustomers: 0,
selectAllField: false,
selectedCustomers: [],
selectedViewCustomer: {},
isFetchingInitialSettings: false,
isFetchingViewData: false,
currentCustomer: {
...customerStub(),
},
editCustomer: null
}),
getters: {
isEdit: (state) => (state.currentCustomer.id ? true : false),
},
actions: {
resetCurrentCustomer() {
this.currentCustomer = {
...customerStub(),
}
},
copyAddress() {
this.currentCustomer.shipping = {
...this.currentCustomer.billing,
type: 'shipping',
}
},
fetchCustomerInitialSettings(isEdit) {
const route = useRoute()
const globalStore = useGlobalStore()
const companyStore = useCompanyStore()
this.isFetchingInitialSettings = true
let editActions = []
if (isEdit) {
editActions = [this.fetchCustomer(route.params.id)]
} else {
this.currentCustomer.currency_id =
companyStore.selectedCompanyCurrency.id
}
Promise.all([
globalStore.fetchCurrencies(),
globalStore.fetchCountries(),
...editActions,
])
.then(async ([res1, res2, res3]) => {
this.isFetchingInitialSettings = false
})
.catch((error) => {
handleError(error)
})
},
fetchCustomers(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/customers`, { params })
.then((response) => {
this.customers = response.data.data
this.totalCustomers = response.data.meta.customer_total_count
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchViewCustomer(params) {
return new Promise((resolve, reject) => {
this.isFetchingViewData = true
http
.get(`/api/v1/customers/${params.id}/stats`, { params })
.then((response) => {
this.selectedViewCustomer = {}
Object.assign(this.selectedViewCustomer, response.data.data)
this.setAddressStub(response.data.data)
this.isFetchingViewData = false
resolve(response)
})
.catch((err) => {
this.isFetchingViewData = false
handleError(err)
reject(err)
})
})
},
fetchCustomer(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/customers/${id}`)
.then((response) => {
Object.assign(this.currentCustomer, response.data.data)
this.setAddressStub(response.data.data)
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addCustomer(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/customers', data)
.then((response) => {
this.customers.push(response.data.data)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('customers.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateCustomer(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/customers/${data.id}`, data)
.then((response) => {
if (response.data) {
let pos = this.customers.findIndex(
(customer) => customer.id === response.data.data.id
)
this.customers[pos] = data
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('customers.updated_message'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteCustomer(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/customers/delete`, id)
.then((response) => {
let index = this.customers.findIndex(
(customer) => customer.id === id
)
this.customers.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('customers.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleCustomers() {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/customers/delete`, { ids: this.selectedCustomers })
.then((response) => {
this.selectedCustomers.forEach((customer) => {
let index = this.customers.findIndex(
(_customer) => _customer.id === customer.id
)
this.customers.splice(index, 1)
})
notificationStore.showNotification({
type: 'success',
message: global.t('customers.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setSelectAllState(data) {
this.selectAllField = data
},
selectCustomer(data) {
this.selectedCustomers = data
if (this.selectedCustomers.length === this.customers.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllCustomers() {
if (this.selectedCustomers.length === this.customers.length) {
this.selectedCustomers = []
this.selectAllField = false
} else {
let allCustomerIds = this.customers.map((customer) => customer.id)
this.selectedCustomers = allCustomerIds
this.selectAllField = true
}
},
setAddressStub(data) {
if (!data.billing) this.currentCustomer.billing = { ...addressStub }
if (!data.shipping) this.currentCustomer.shipping = { ...addressStub }
},
},
})()
}

View File

@@ -1,85 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { handleError } from '@/scripts/helpers/error-handling'
export const useDashboardStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('dashboard', {
state: () => ({
stats: {
totalAmountDue: 0,
totalCustomerCount: 0,
totalInvoiceCount: 0,
totalEstimateCount: 0,
},
chartData: {
months: [],
invoiceTotals: [],
expenseTotals: [],
receiptTotals: [],
netIncomeTotals: [],
},
totalSales: null,
totalReceipts: null,
totalExpenses: null,
totalNetIncome: null,
recentDueInvoices: [],
recentEstimates: [],
isDashboardDataLoaded: false,
}),
actions: {
loadData(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/dashboard`, { params })
.then((response) => {
// Stats
this.stats.totalAmountDue = response.data.total_amount_due
this.stats.totalCustomerCount = response.data.total_customer_count
this.stats.totalInvoiceCount = response.data.total_invoice_count
this.stats.totalEstimateCount = response.data.total_estimate_count
// Dashboard Chart
if (this.chartData && response.data.chart_data) {
this.chartData.months = response.data.chart_data.months
this.chartData.invoiceTotals =
response.data.chart_data.invoice_totals
this.chartData.expenseTotals =
response.data.chart_data.expense_totals
this.chartData.receiptTotals =
response.data.chart_data.receipt_totals
this.chartData.netIncomeTotals =
response.data.chart_data.net_income_totals
}
// Dashboard Chart Labels
this.totalSales = response.data.total_sales
this.totalReceipts = response.data.total_receipts
this.totalExpenses = response.data.total_expenses
this.totalNetIncome = response.data.total_net_income
// Dashboard Table Data
this.recentDueInvoices = response.data.recent_due_invoices
this.recentEstimates = response.data.recent_estimates
this.isDashboardDataLoaded = true
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,192 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useDiskStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('disk', {
state: () => ({
disks: [],
diskDrivers: [],
diskConfigData: null,
selected_driver: 'local',
doSpaceDiskConfig: {
name: '',
selected_driver: 'doSpaces',
key: '',
secret: '',
region: '',
bucket: '',
endpoint: '',
root: '',
},
dropBoxDiskConfig: {
name: '',
selected_driver: 'dropbox',
token: '',
key: '',
secret: '',
app: '',
},
localDiskConfig: {
name: '',
selected_driver: 'local',
root: '',
},
s3DiskConfigData: {
name: '',
selected_driver: 's3',
key: '',
secret: '',
region: '',
bucket: '',
root: '',
},
s3CompatDiskConfigData: {
name: '',
selected_driver: 's3compat',
key: '',
secret: '',
region: '',
bucket: '',
root: '',
endpoint: '',
},
}),
getters: {
getDiskDrivers: (state) => state.diskDrivers,
},
actions: {
fetchDiskEnv(data) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/disks/${data.disk}`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchDisks(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/disks`, { params })
.then((response) => {
this.disks = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchDiskDrivers() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/disk/drivers`)
.then((response) => {
this.diskConfigData = response.data
this.diskDrivers = response.data.drivers
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteFileDisk(id) {
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/disks/${id}`)
.then((response) => {
if (response.data.success) {
let index = this.disks.findIndex(
(category) => category.id === id
)
this.disks.splice(index, 1)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.disk.deleted_message'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateDisk(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/disks/${data.id}`, data)
.then((response) => {
if (response.data) {
let pos = this.disks.findIndex(
(disk) => disk.id === response.data.data
)
this.disks[pos] = data.disks
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.disk.success_set_default_disk'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
createDisk(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/disks`, data)
.then((response) => {
if (response.data) {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('settings.disk.success_create'),
})
}
this.disks.push(response.data)
resolve(response)
})
.catch((err) => {
/* notificationStore.showNotification({
type: 'error',
message: global.t('settings.disk.invalid_disk_credentials'),
}) */
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,644 +0,0 @@
import http from '@/scripts/http'
import moment from 'moment'
import Guid from 'guid'
import _ from 'lodash'
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
import { useCompanyStore } from './company'
import { useCustomerStore } from './customer'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useItemStore } from './item'
import { useTaxTypeStore } from './tax-type'
import { handleError } from '@/scripts/helpers/error-handling'
import estimateStub from '../stub/estimate'
import estimateItemStub from '../stub/estimate-item'
import taxStub from '../stub/tax'
import { useUserStore } from './user'
import { useNotesStore } from './note'
export const useEstimateStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('estimate', {
state: () => ({
templates: [],
estimates: [],
selectAllField: false,
selectedEstimates: [],
totalEstimateCount: 0,
isFetchingInitialSettings: false,
showExchangeRate: false,
newEstimate: {
...estimateStub(),
},
}),
getters: {
getSubTotal() {
return this.newEstimate.items.reduce(function (a, b) {
return a + b['total']
}, 0)
},
getNetTotal() {
return this.getSubtotalWithDiscount - this.getTotalTax
},
getTotalSimpleTax() {
return _.sumBy(this.newEstimate.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
}
return 0
})
},
getTotalCompoundTax() {
return _.sumBy(this.newEstimate.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
}
return 0
})
},
getTotalTax() {
if (
this.newEstimate.tax_per_item === 'NO' ||
this.newEstimate.tax_per_item === null
) {
return this.getTotalSimpleTax + this.getTotalCompoundTax
}
return _.sumBy(this.newEstimate.items, function (tax) {
return tax.tax
})
},
getSubtotalWithDiscount() {
return this.getSubTotal - this.newEstimate.discount_val
},
getTotal() {
if (this.newEstimate.tax_included) {
return this.getSubtotalWithDiscount
}
return this.getSubtotalWithDiscount + this.getTotalTax
},
isEdit: (state) => (state.newEstimate.id ? true : false),
},
actions: {
resetCurrentEstimate() {
this.newEstimate = {
...estimateStub(),
}
},
previewEstimate(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/estimates/${params.id}/send/preview`, { params })
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchEstimates(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/estimates`, { params })
.then((response) => {
this.estimates = response.data.data
this.totalEstimateCount = response.data.meta.estimate_total_count
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
getNextNumber(params, setState = false) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/next-number?key=estimate`, { params })
.then((response) => {
if (setState) {
this.newEstimate.estimate_number = response.data.nextNumber
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchEstimate(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/estimates/${id}`)
.then((response) => {
this.setEstimateData(response.data.data)
this.setCustomerAddresses(this.newEstimate.customer)
resolve(response)
})
.catch((err) => {
console.log(err)
handleError(err)
reject(err)
})
})
},
setEstimateData(estimate) {
Object.assign(this.newEstimate, estimate)
if (this.newEstimate.tax_per_item === 'YES') {
this.newEstimate.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length) {
_i.taxes.push({ ...taxStub, id: Guid.raw() })
}
})
}
if (this.newEstimate.discount_per_item === 'YES') {
this.newEstimate.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed') {
this.newEstimate.items[index].discount = _i.discount / 100
}
})
} else {
if (this.newEstimate.discount_type === 'fixed') {
this.newEstimate.discount = this.newEstimate.discount / 100
}
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address) {
this.newEstimate.customer.billing_address =
customer_business.billing_address
}
if (customer_business?.shipping_address) {
this.newEstimate.customer.shipping_address =
customer_business.shipping_address
}
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }
let found = this.newEstimate.taxes.find(
(_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE',
)
if (found) {
for (const key in found) {
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
salesTax[key] = found[key]
}
}
salesTax.id = found.tax_type_id
console.log(salesTax, 'salesTax')
taxTypeStore.taxTypes.push(salesTax)
console.log(taxTypeStore.taxTypes)
}
},
sendEstimate(data) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${data.id}/send`, data)
.then((response) => {
if (!data.is_preview) {
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.send_estimate_successfully'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addEstimate(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/estimates', data)
.then((response) => {
this.estimates = [...this.estimates, response.data.estimate]
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteEstimate(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/delete`, id)
.then((response) => {
let index = this.estimates.findIndex(
(estimate) => estimate.id === id,
)
this.estimates.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleEstimates(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/delete`, { ids: this.selectedEstimates })
.then((response) => {
this.selectedEstimates.forEach((estimate) => {
let index = this.estimates.findIndex(
(_est) => _est.id === estimate.id,
)
this.estimates.splice(index, 1)
})
this.selectedEstimates = []
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateEstimate(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/estimates/${data.id}`, data)
.then((response) => {
let pos = this.estimates.findIndex(
(estimate) => estimate.id === response.data.data.id,
)
this.estimates[pos] = response.data.data
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
cloneEstimate(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${data.id}/clone`, data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.cloned_successfully'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
markAsAccepted(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${data.id}/status`, data)
.then((response) => {
let pos = this.estimates.findIndex(
(estimate) => estimate.id === data.id,
)
if (this.estimates[pos]) {
this.estimates[pos].status = 'ACCEPTED'
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.marked_as_accepted_message'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
markAsRejected(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${data.id}/status`, data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.marked_as_rejected_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
markAsSent(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${data.id}/status`, data)
.then((response) => {
let pos = this.estimates.findIndex(
(estimate) => estimate.id === data.id,
)
if (this.estimates[pos]) {
this.estimates[pos].status = 'SENT'
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.mark_as_sent_successfully'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
convertToInvoice(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/estimates/${id}/convert-to-invoice`)
.then((response) => {
notificationStore.showNotification({
type: 'success',
message: global.t('estimates.conversion_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
searchEstimate(data) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/estimates?${data}`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
selectEstimate(data) {
this.selectedEstimates = data
if (this.selectedEstimates.length === this.estimates.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllEstimates() {
if (this.selectedEstimates.length === this.estimates.length) {
this.selectedEstimates = []
this.selectAllField = false
} else {
let allEstimateIds = this.estimates.map((estimate) => estimate.id)
this.selectedEstimates = allEstimateIds
this.selectAllField = true
}
},
selectCustomer(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/customers/${id}`)
.then((response) => {
this.newEstimate.customer = response.data.data
this.newEstimate.customer_id = response.data.data.id
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchEstimateTemplates(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/estimates/templates`, { params })
.then((response) => {
this.templates = response.data.estimateTemplates
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setTemplate(data) {
this.newEstimate.template_name = data
},
resetSelectedCustomer() {
this.newEstimate.customer = null
this.newEstimate.customer_id = ''
},
selectNote(data) {
this.newEstimate.selectedNote = null
this.newEstimate.selectedNote = data
},
resetSelectedNote() {
this.newEstimate.selectedNote = null
},
addItem() {
this.newEstimate.items.push({
...estimateItemStub,
id: Guid.raw(),
taxes: [{ ...taxStub, id: Guid.raw() }],
})
},
updateItem(data) {
Object.assign(this.newEstimate.items[data.index], { ...data })
},
removeItem(index) {
this.newEstimate.items.splice(index, 1)
},
deselectItem(index) {
this.newEstimate.items[index] = {
...estimateItemStub,
id: Guid.raw(),
taxes: [{ ...taxStub, id: Guid.raw() }],
}
},
async fetchEstimateInitialSettings(isEdit) {
const companyStore = useCompanyStore()
const customerStore = useCustomerStore()
const itemStore = useItemStore()
const taxTypeStore = useTaxTypeStore()
const route = useRoute()
const userStore = useUserStore()
const notesStore = useNotesStore()
this.isFetchingInitialSettings = true
this.newEstimate.selectedCurrency = companyStore.selectedCompanyCurrency
if (route.query.customer) {
let response = await customerStore.fetchCustomer(route.query.customer)
this.newEstimate.customer = response.data.data
this.newEstimate.customer_id = response.data.data.id
}
let editActions = []
if (!isEdit) {
await notesStore.fetchNotes()
this.newEstimate.notes =
notesStore.getDefaultNoteForType('Estimate')?.notes
this.newEstimate.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item
this.newEstimate.sales_tax_type =
companyStore.selectedCompanySettings.sales_tax_type
this.newEstimate.sales_tax_address_type =
companyStore.selectedCompanySettings.sales_tax_address_type
this.newEstimate.discount_per_item =
companyStore.selectedCompanySettings.discount_per_item
this.newEstimate.estimate_date = moment().format('YYYY-MM-DD')
if (
companyStore.selectedCompanySettings
.estimate_set_expiry_date_automatically === 'YES'
) {
this.newEstimate.expiry_date = moment()
.add(
companyStore.selectedCompanySettings.estimate_expiry_date_days,
'days',
)
.format('YYYY-MM-DD')
}
} else {
editActions = [this.fetchEstimate(route.params.id)]
}
Promise.all([
itemStore.fetchItems({
filter: {},
orderByField: '',
orderBy: '',
}),
this.resetSelectedNote(),
this.fetchEstimateTemplates(),
this.getNextNumber(),
taxTypeStore.fetchTaxTypes({ limit: 'all' }),
...editActions,
])
.then(async ([res1, res2, res3, res4, res5, res6, res7]) => {
// Create
if (!isEdit) {
if (res4.data) {
this.newEstimate.estimate_number = res4.data.nextNumber
}
this.setTemplate(this.templates[0].name)
this.newEstimate.template_name = userStore.currentUserSettings
.default_estimate_template
? userStore.currentUserSettings.default_estimate_template
: this.newEstimate.template_name
}
if (isEdit) {
this.addSalesTaxUs()
}
this.isFetchingInitialSettings = false
})
.catch((err) => {
handleError(err)
this.isFetchingInitialSettings = false
})
},
},
})()
}

View File

@@ -1,247 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useExchangeRateStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
const notificationStore = useNotificationStore()
return defineStoreFunc('exchange-rate', {
state: () => ({
supportedCurrencies: [],
drivers: [],
activeUsedCurrencies: [],
providers: [],
currencies: null,
currentExchangeRate: {
id: null,
driver: '',
key: '',
active: true,
currencies: [],
},
currencyConverter: {
type: '',
url: '',
},
bulkCurrencies: [],
}),
getters: {
isEdit: (state) => (state.currentExchangeRate.id ? true : false),
},
actions: {
fetchProviders(params) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/exchange-rate-providers', { params })
.then((response) => {
this.providers = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchDefaultProviders() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/config?key=exchange_rate_drivers`)
.then((response) => {
this.drivers = response.data.exchange_rate_drivers
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchProvider(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/exchange-rate-providers/${id}`)
.then((response) => {
this.currentExchangeRate = response.data.data
this.currencyConverter = response.data.data.driver_config
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addProvider(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/exchange-rate-providers', data)
.then((response) => {
notificationStore.showNotification({
type: 'success',
message: global.t('settings.exchange_rate.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateProvider(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/exchange-rate-providers/${data.id}`, data)
.then((response) => {
notificationStore.showNotification({
type: 'success',
message: global.t('settings.exchange_rate.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteExchangeRate(id) {
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/exchange-rate-providers/${id}`)
.then((response) => {
let index = this.drivers.findIndex((driver) => driver.id === id)
this.drivers.splice(index, 1)
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('settings.exchange_rate.deleted_message'),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('settings.exchange_rate.error'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCurrencies(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/supported-currencies`, { params })
.then((response) => {
this.supportedCurrencies = response.data.supportedCurrencies
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchActiveCurrency(params) {
return new Promise((resolve, reject) => {
http
.get('/api/v1/used-currencies', { params })
.then((response) => {
this.activeUsedCurrencies = response.data.activeUsedCurrencies
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchBulkCurrencies() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/currencies/used')
.then((response) => {
this.bulkCurrencies = response.data.currencies.map((_m) => {
_m.exchange_rate = null
return _m
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateBulkExchangeRate(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/currencies/bulk-update-exchange-rate', data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
getCurrentExchangeRate(currencyId) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/currencies/${currencyId}/exchange-rate`)
.then((response) => {
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
getCurrencyConverterServers() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/config?key=currency_converter_servers')
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
checkForActiveProvider(currency_id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/currencies/${currency_id}/active-provider`)
.then((response) => {
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
},
})()
}

View File

@@ -1,238 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { handleError } from '@/scripts/helpers/error-handling'
import { useNotificationStore } from '@/scripts/stores/notification'
import expenseStub from '@/scripts/admin/stub/expense'
import utils from '@/scripts/helpers/utilities'
export const useExpenseStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('expense', {
state: () => ({
expenses: [],
totalExpenses: 0,
selectAllField: false,
selectedExpenses: [],
paymentModes: [],
showExchangeRate: false,
currentExpense: {
...expenseStub,
},
}),
getters: {
getCurrentExpense: (state) => state.currentExpense,
getSelectedExpenses: (state) => state.selectedExpenses,
},
actions: {
resetCurrentExpenseData() {
this.currentExpense = {
...expenseStub,
}
},
fetchExpenses(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/expenses`, { params })
.then((response) => {
this.expenses = response.data.data
this.totalExpenses = response.data.meta.expense_total_count
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchExpense(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/expenses/${id}`)
.then((response) => {
if (response.data) {
Object.assign(this.currentExpense, response.data.data)
this.currentExpense.selectedCurrency =
response.data.data.currency
this.currentExpense.attachment_receipt = null
if (response.data.data.attachment_receipt_url) {
if (
utils.isImageFile(
response.data.data.attachment_receipt_meta.mime_type
)
) {
this.currentExpense.receiptFiles = [
{ image: `/reports/expenses/${id}/receipt?${response.data.data.attachment_receipt_meta.uuid}` },
]
} else {
this.currentExpense.receiptFiles = [
{
type: 'document',
name: response.data.data.attachment_receipt_meta
.file_name,
},
]
}
} else {
this.currentExpense.receiptFiles = []
}
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addExpense(data) {
const formData = utils.toFormData(data)
return new Promise((resolve, reject) => {
http
.post('/api/v1/expenses', formData)
.then((response) => {
this.expenses.push(response.data)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('expenses.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateExpense({ id, data, isAttachmentReceiptRemoved }) {
const notificationStore = useNotificationStore()
const formData = utils.toFormData(data)
formData.append('_method', 'PUT')
formData.append('is_attachment_receipt_removed', isAttachmentReceiptRemoved)
return new Promise((resolve) => {
http.post(`/api/v1/expenses/${id}`, formData).then((response) => {
let pos = this.expenses.findIndex(
(expense) => expense.id === response.data.id
)
this.expenses[pos] = data.expense
notificationStore.showNotification({
type: 'success',
message: global.t('expenses.updated_message'),
})
resolve(response)
})
}).catch((err) => {
handleError(err)
reject(err)
})
},
setSelectAllState(data) {
this.selectAllField = data
},
selectExpense(data) {
this.selectedExpenses = data
if (this.selectedExpenses.length === this.expenses.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllExpenses(data) {
if (this.selectedExpenses.length === this.expenses.length) {
this.selectedExpenses = []
this.selectAllField = false
} else {
let allExpenseIds = this.expenses.map((expense) => expense.id)
this.selectedExpenses = allExpenseIds
this.selectAllField = true
}
},
deleteExpense(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/expenses/delete`, id)
.then((response) => {
let index = this.expenses.findIndex(
(expense) => expense.id === id
)
this.expenses.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('expenses.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleExpenses() {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/expenses/delete`, { ids: this.selectedExpenses })
.then((response) => {
this.selectedExpenses.forEach((expense) => {
let index = this.expenses.findIndex(
(_expense) => _expense.id === expense.id
)
this.expenses.splice(index, 1)
})
notificationStore.showNotification({
type: 'success',
message: global.t('expenses.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchPaymentModes(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/payment-methods`, { params })
.then((response) => {
this.paymentModes = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,309 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useCompanyStore } from './company'
import { useUserStore } from './user'
import { useModuleStore } from './module'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
import _ from 'lodash'
export const useGlobalStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('global', {
state: () => ({
// Global Configuration
config: null,
globalSettings: null,
// Global Lists
timeZones: [],
dateFormats: [],
timeFormats: [],
currencies: [],
countries: [],
languages: [],
fiscalYears: [],
// Menus
mainMenu: [],
settingMenu: [],
// Boolean Flags
isAppLoaded: false,
isSidebarOpen: false,
isSidebarCollapsed: window.Ls?.get('sidebarCollapsed') === 'true',
areCurrenciesLoading: false,
downloadReport: null,
}),
getters: {
menuGroups: (state) => {
return Object.values(_.groupBy(state.mainMenu, 'group'))
},
},
actions: {
bootstrap() {
return new Promise((resolve, reject) => {
const companyStore = useCompanyStore()
const url = companyStore.isAdminMode
? '/api/v1/bootstrap?admin_mode=1'
: '/api/v1/bootstrap'
http
.get(url)
.then(async (response) => {
const userStore = useUserStore()
const moduleStore = useModuleStore()
this.mainMenu = response.data.main_menu
this.settingMenu = response.data.setting_menu
this.config = response.data.config
this.globalSettings = response.data.global_settings
// user store
userStore.currentUser = response.data.current_user
userStore.currentUserSettings =
response.data.current_user_settings
userStore.currentAbilities = response.data.current_user_abilities
// Module store
moduleStore.apiToken = response.data.global_settings.api_token
moduleStore.enableModules = response.data.modules
// invitation store
if (response.data.pending_invitations) {
const { useInvitationStore } = await import('@/scripts/admin/stores/invitation')
const invitationStore = useInvitationStore()
invitationStore.setPendingInvitations(response.data.pending_invitations)
}
// company store
companyStore.companies = response.data.companies
if (response.data.current_company) {
companyStore.setSelectedCompany(response.data.current_company)
companyStore.selectedCompanySettings =
response.data.current_company_settings
companyStore.selectedCompanyCurrency =
response.data.current_company_currency
} else {
companyStore.setSelectedCompany(null)
companyStore.selectedCompanySettings = {}
companyStore.selectedCompanyCurrency = null
}
// Determine and load the appropriate language
const userLanguage = response.data.current_user_settings?.language
const companyLanguage = response.data.current_company_settings?.language
const targetLanguage = userLanguage || companyLanguage || 'en'
// Load the language dynamically if it's not English
if (targetLanguage !== 'en' && window.loadLanguage) {
try {
await window.loadLanguage(targetLanguage)
} catch (error) {
console.warn('Failed to load language during bootstrap:', error)
// Fall back to English if loading fails
if (typeof global.locale !== 'string') {
global.locale.value = 'en'
} else {
global.locale = 'en'
}
}
} else {
// Set locale for English or when loadLanguage is not available
if (typeof global.locale !== 'string') {
global.locale.value = targetLanguage
} else {
global.locale = targetLanguage
}
}
this.isAppLoaded = true
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchCurrencies() {
return new Promise((resolve, reject) => {
if (this.currencies.length || this.areCurrenciesLoading) {
resolve(this.currencies)
} else {
this.areCurrenciesLoading = true
http
.get('/api/v1/currencies')
.then((response) => {
this.currencies = response.data.data.filter((currency) => {
return (currency.name = `${currency.code} - ${currency.name}`)
})
this.areCurrenciesLoading = false
resolve(response)
})
.catch((err) => {
handleError(err)
this.areCurrenciesLoading = false
reject(err)
})
}
})
},
fetchConfig(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/config`, { params })
.then((response) => {
if (response.data.languages) {
this.languages = response.data.languages
} else {
this.fiscalYears = response.data.fiscal_years
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchDateFormats() {
return new Promise((resolve, reject) => {
if (this.dateFormats.length) {
resolve(this.dateFormats)
} else {
http
.get('/api/v1/date/formats')
.then((response) => {
this.dateFormats = response.data.date_formats
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
},
fetchTimeFormats() {
return new Promise((resolve, reject) => {
if (this.timeFormats.length) {
resolve(this.timeFormats)
} else {
http
.get('/api/v1/time/formats')
.then((response) => {
this.timeFormats = response.data.time_formats
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
},
fetchTimeZones() {
return new Promise((resolve, reject) => {
if (this.timeZones.length) {
resolve(this.timeZones)
} else {
http
.get('/api/v1/timezones')
.then((response) => {
this.timeZones = response.data.time_zones
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
},
fetchCountries() {
return new Promise((resolve, reject) => {
if (this.countries.length) {
resolve(this.countries)
} else {
http
.get('/api/v1/countries')
.then((response) => {
this.countries = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
},
fetchPlaceholders(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/number-placeholders`, { params })
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setSidebarVisibility(val) {
this.isSidebarOpen = val
},
toggleSidebarCollapse() {
this.isSidebarCollapsed = !this.isSidebarCollapsed
window.Ls.set('sidebarCollapsed', this.isSidebarCollapsed ? 'true' : 'false')
},
setIsAppLoaded(isAppLoaded) {
this.isAppLoaded = isAppLoaded
},
updateGlobalSettings({ data, message }) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/settings', data)
.then((response) => {
Object.assign(this.globalSettings, data.settings)
if (message) {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t(message),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,200 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useCompanyStore } from './company'
import { handleError } from '@/scripts/helpers/error-handling'
export const useInstallationStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
const companyStore = useCompanyStore()
return defineStoreFunc('installation', {
state: () => ({
currentDataBaseData: {
database_connection: 'mysql',
database_hostname: '127.0.0.1',
database_port: '3306',
database_name: null,
database_username: null,
database_password: null,
database_overwrite: false,
app_url: window.location.origin,
app_locale: null
},
}),
actions: {
fetchInstallationLanguages() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/installation/languages`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInstallationRequirements() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/installation/requirements`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInstallationStep() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/installation/wizard-step`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addInstallationStep(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/installation/wizard-step`, data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addInstallationLanguage(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/installation/wizard-language`, data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInstallationPermissions() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/installation/permissions`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInstallationDatabase(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/installation/database/config`, { params })
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addInstallationDatabase(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/installation/database/config`, data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addInstallationFinish() {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/installation/finish`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setInstallationDomain(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/installation/set-domain`, data)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
installationLogin() {
return new Promise((resolve, reject) => {
http.get('/sanctum/csrf-cookie').then((response) => {
if (response) {
http
.post('/api/v1/installation/login')
.then((response) => {
companyStore.setSelectedCompany(response.data.company)
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
})
},
checkAuthenticated() {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/auth/check`)
.then((response) => {
resolve(response)
})
.catch((err) => {
reject(err)
})
})
},
},
})()
}

View File

@@ -1,56 +0,0 @@
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import http from '@/scripts/http'
export const useInvitationStore = defineStore('invitation', {
state: () => ({
pendingInvitations: [],
}),
actions: {
setPendingInvitations(invitations) {
this.pendingInvitations = invitations
},
async fetchPending() {
const response = await http.get('/api/v1/invitations/pending')
this.pendingInvitations = response.data.invitations
return response
},
async accept(token) {
const notificationStore = useNotificationStore()
const globalStore = useGlobalStore()
const response = await http.post(`/api/v1/invitations/${token}/accept`)
notificationStore.showNotification({
type: 'success',
message: 'Invitation accepted!',
})
// Refresh bootstrap to get updated companies list
await globalStore.bootstrap()
return response
},
async decline(token) {
const notificationStore = useNotificationStore()
const response = await http.post(`/api/v1/invitations/${token}/decline`)
this.pendingInvitations = this.pendingInvitations.filter(
(inv) => inv.token !== token
)
notificationStore.showNotification({
type: 'success',
message: 'Invitation declined.',
})
return response
},
},
})

View File

@@ -1,579 +0,0 @@
import http from '@/scripts/http'
import moment from 'moment'
import Guid from 'guid'
import _ from 'lodash'
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
import { handleError } from '@/scripts/helpers/error-handling'
import invoiceItemStub from '../stub/invoice-item'
import taxStub from '../stub/tax'
import invoiceStub from '../stub/invoice'
import { useNotificationStore } from '@/scripts/stores/notification'
import { useCustomerStore } from './customer'
import { useTaxTypeStore } from './tax-type'
import { useCompanyStore } from './company'
import { useItemStore } from './item'
import { useUserStore } from './user'
import { useNotesStore } from './note'
export const useInvoiceStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
const notificationStore = useNotificationStore()
return defineStoreFunc('invoice', {
state: () => ({
templates: [],
invoices: [],
selectedInvoices: [],
selectAllField: false,
invoiceTotalCount: 0,
showExchangeRate: false,
isFetchingInitialSettings: false,
isFetchingInvoice: false,
newInvoice: {
...invoiceStub(),
},
}),
getters: {
getInvoice: (state) => (id) => {
let invId = parseInt(id)
return state.invoices.find((invoice) => invoice.id === invId)
},
getSubTotal() {
return this.newInvoice.items.reduce(function (a, b) {
return a + b['total']
}, 0)
},
getNetTotal() {
return this.getSubtotalWithDiscount - this.getTotalTax
},
getTotalSimpleTax() {
return _.sumBy(this.newInvoice.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
}
return 0
})
},
getTotalCompoundTax() {
return _.sumBy(this.newInvoice.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
}
return 0
})
},
getTotalTax() {
if (
this.newInvoice.tax_per_item === 'NO' ||
this.newInvoice.tax_per_item === null
) {
return this.getTotalSimpleTax + this.getTotalCompoundTax
}
return _.sumBy(this.newInvoice.items, function (tax) {
return tax.tax
})
},
getSubtotalWithDiscount() {
return this.getSubTotal - this.newInvoice.discount_val
},
getTotal() {
if (this.newInvoice.tax_included) {
return this.getSubtotalWithDiscount
}
return this.getSubtotalWithDiscount + this.getTotalTax
},
isEdit: (state) => (state.newInvoice.id ? true : false),
},
actions: {
resetCurrentInvoice() {
this.newInvoice = {
...invoiceStub(),
}
},
previewInvoice(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/invoices/${params.id}/send/preview`, { params })
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInvoices(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/invoices`, { params })
.then((response) => {
this.invoices = response.data.data
this.invoiceTotalCount = response.data.meta.invoice_total_count
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInvoice(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/invoices/${id}`)
.then((response) => {
this.setInvoiceData(response.data.data)
this.setCustomerAddresses(this.newInvoice.customer)
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setInvoiceData(invoice) {
Object.assign(this.newInvoice, invoice)
if (this.newInvoice.tax_per_item === 'YES') {
this.newInvoice.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length)
_i.taxes.push({ ...taxStub, id: Guid.raw() })
})
}
if (this.newInvoice.discount_per_item === 'YES') {
this.newInvoice.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed')
this.newInvoice.items[index].discount = _i.discount / 100
})
} else {
if (this.newInvoice.discount_type === 'fixed')
this.newInvoice.discount = this.newInvoice.discount / 100
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address)
this.newInvoice.customer.billing_address =
customer_business.billing_address
if (customer_business?.shipping_address)
this.newInvoice.customer.shipping_address =
customer_business.shipping_address
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }
let found = this.newInvoice.taxes.find(
(_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE',
)
if (found) {
for (const key in found) {
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
salesTax[key] = found[key]
}
}
salesTax.id = found.tax_type_id
taxTypeStore.taxTypes.push(salesTax)
}
},
sendInvoice(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/invoices/${data.id}/send`, data)
.then((response) => {
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.invoice_sent_successfully'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addInvoice(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/invoices', data)
.then((response) => {
this.invoices = [...this.invoices, response.data.invoice]
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteInvoice(id) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/invoices/delete`, id)
.then((response) => {
let index = this.invoices.findIndex(
(invoice) => invoice.id === id,
)
this.invoices.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleInvoices(id) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/invoices/delete`, { ids: this.selectedInvoices })
.then((response) => {
this.selectedInvoices.forEach((invoice) => {
let index = this.invoices.findIndex(
(_inv) => _inv.id === invoice.id,
)
this.invoices.splice(index, 1)
})
this.selectedInvoices = []
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateInvoice(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/invoices/${data.id}`, data)
.then((response) => {
let pos = this.invoices.findIndex(
(invoice) => invoice.id === response.data.data.id,
)
this.invoices[pos] = response.data.data
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
cloneInvoice(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/invoices/${data.id}/clone`, data)
.then((response) => {
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.cloned_successfully'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
markAsSent(data) {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/invoices/${data.id}/status`, data)
.then((response) => {
let pos = this.invoices.findIndex(
(invoices) => invoices.id === data.id,
)
if (this.invoices[pos]) {
this.invoices[pos].status = 'SENT'
}
notificationStore.showNotification({
type: 'success',
message: global.t('invoices.mark_as_sent_successfully'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
getNextNumber(params, setState = false) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/next-number?key=invoice`, { params })
.then((response) => {
if (setState) {
this.newInvoice.invoice_number = response.data.nextNumber
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
searchInvoice(data) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/invoices?${data}`)
.then((response) => {
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
selectInvoice(data) {
this.selectedInvoices = data
if (this.selectedInvoices.length === this.invoices.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllInvoices() {
if (this.selectedInvoices.length === this.invoices.length) {
this.selectedInvoices = []
this.selectAllField = false
} else {
let allInvoiceIds = this.invoices.map((invoice) => invoice.id)
this.selectedInvoices = allInvoiceIds
this.selectAllField = true
}
},
selectCustomer(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/customers/${id}`)
.then((response) => {
this.newInvoice.customer = response.data.data
this.newInvoice.customer_id = response.data.data.id
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchInvoiceTemplates(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/invoices/templates`, { params })
.then((response) => {
this.templates = response.data.invoiceTemplates
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
selectNote(data) {
this.newInvoice.selectedNote = null
this.newInvoice.selectedNote = data
},
setTemplate(data) {
this.newInvoice.template_name = data
},
resetSelectedCustomer() {
this.newInvoice.customer = null
this.newInvoice.customer_id = null
},
addItem() {
this.newInvoice.items.push({
...invoiceItemStub,
id: Guid.raw(),
taxes: [{ ...taxStub, id: Guid.raw() }],
})
},
updateItem(data) {
Object.assign(this.newInvoice.items[data.index], { ...data })
},
removeItem(index) {
this.newInvoice.items.splice(index, 1)
},
deselectItem(index) {
this.newInvoice.items[index] = {
...invoiceItemStub,
id: Guid.raw(),
taxes: [{ ...taxStub, id: Guid.raw() }],
}
},
resetSelectedNote() {
this.newInvoice.selectedNote = null
},
// On Load actions
async fetchInvoiceInitialSettings(isEdit) {
const companyStore = useCompanyStore()
const customerStore = useCustomerStore()
const itemStore = useItemStore()
const taxTypeStore = useTaxTypeStore()
const route = useRoute()
const userStore = useUserStore()
const notesStore = useNotesStore()
this.isFetchingInitialSettings = true
this.newInvoice.selectedCurrency = companyStore.selectedCompanyCurrency
if (route.query.customer) {
let response = await customerStore.fetchCustomer(route.query.customer)
this.newInvoice.customer = response.data.data
this.newInvoice.customer_id = response.data.data.id
}
let editActions = []
if (!isEdit) {
await notesStore.fetchNotes()
this.newInvoice.notes =
notesStore.getDefaultNoteForType('Invoice')?.notes
this.newInvoice.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item
this.newInvoice.sales_tax_type =
companyStore.selectedCompanySettings.sales_tax_type
this.newInvoice.sales_tax_address_type =
companyStore.selectedCompanySettings.sales_tax_address_type
this.newInvoice.discount_per_item =
companyStore.selectedCompanySettings.discount_per_item
let dateFormat = 'YYYY-MM-DD'
if (companyStore.selectedCompanySettings.invoice_use_time === 'YES') {
dateFormat += ' HH:mm'
}
this.newInvoice.invoice_date = moment().format(dateFormat)
if (
companyStore.selectedCompanySettings
.invoice_set_due_date_automatically === 'YES'
) {
this.newInvoice.due_date = moment()
.add(
companyStore.selectedCompanySettings.invoice_due_date_days,
'days',
)
.format('YYYY-MM-DD')
}
} else {
editActions = [this.fetchInvoice(route.params.id)]
}
Promise.all([
itemStore.fetchItems({
filter: {},
orderByField: '',
orderBy: '',
}),
this.resetSelectedNote(),
this.fetchInvoiceTemplates(),
this.getNextNumber(),
taxTypeStore.fetchTaxTypes({ limit: 'all' }),
...editActions,
])
.then(async ([res1, res2, res3, res4, res5, res6]) => {
if (!isEdit) {
if (res4.data) {
this.newInvoice.invoice_number = res4.data.nextNumber
}
if (res3.data) {
this.setTemplate(this.templates[0].name)
this.newInvoice.template_name = userStore.currentUserSettings
.default_invoice_template
? userStore.currentUserSettings.default_invoice_template
: this.newInvoice.template_name
}
}
if (isEdit) {
this.addSalesTaxUs()
}
this.isFetchingInitialSettings = false
})
.catch((err) => {
handleError(err)
reject(err)
})
},
},
})()
}

View File

@@ -1,335 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useItemStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('item', {
state: () => ({
items: [],
totalItems: 0,
selectAllField: false,
selectedItems: [],
itemUnits: [],
currentItemUnit: {
id: null,
name: '',
},
currentItem: {
name: '',
description: '',
price: 0,
unit_id: '',
unit: null,
taxes: [],
tax_per_item: false,
},
}),
getters: {
isItemUnitEdit: (state) => (state.currentItemUnit.id ? true : false),
},
actions: {
resetCurrentItem() {
this.currentItem = {
name: '',
description: '',
price: 0,
unit_id: '',
unit: null,
taxes: [],
}
},
fetchItems(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/items`, { params })
.then((response) => {
this.items = response.data.data
this.totalItems = response.data.meta.item_total_count
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchItem(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/items/${id}`)
.then((response) => {
if (response.data) {
Object.assign(this.currentItem, response.data.data)
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addItem(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/items', data)
.then((response) => {
const notificationStore = useNotificationStore()
this.items.push(response.data.data)
notificationStore.showNotification({
type: 'success',
message: global.t('items.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateItem(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/items/${data.id}`, data)
.then((response) => {
if (response.data) {
const notificationStore = useNotificationStore()
let pos = this.items.findIndex(
(item) => item.id === response.data.data.id
)
this.items[pos] = data.item
notificationStore.showNotification({
type: 'success',
message: global.t('items.updated_message'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteItem(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/items/delete`, id)
.then((response) => {
let index = this.items.findIndex((item) => item.id === id)
this.items.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('items.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleItems() {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/items/delete`, { ids: this.selectedItems })
.then((response) => {
this.selectedItems.forEach((item) => {
let index = this.items.findIndex(
(_item) => _item.id === item.id
)
this.items.splice(index, 1)
})
notificationStore.showNotification({
type: 'success',
message: global.t('items.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
selectItem(data) {
this.selectedItems = data
if (this.selectedItems.length === this.items.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllItems(data) {
if (this.selectedItems.length === this.items.length) {
this.selectedItems = []
this.selectAllField = false
} else {
let allItemIds = this.items.map((item) => item.id)
this.selectedItems = allItemIds
this.selectAllField = true
}
},
addItemUnit(data) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/units`, data)
.then((response) => {
this.itemUnits.push(response.data.data)
if (response.data.data) {
notificationStore.showNotification({
type: 'success',
message: global.t(
'settings.customization.items.item_unit_added'
),
})
}
if (response.data.errors) {
notificationStore.showNotification({
type: 'error',
message: err.response.data.errors[0],
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateItemUnit(data) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.put(`/api/v1/units/${data.id}`, data)
.then((response) => {
let pos = this.itemUnits.findIndex(
(unit) => unit.id === response.data.data.id
)
this.itemUnits[pos] = data
if (response.data.data) {
notificationStore.showNotification({
type: 'success',
message: global.t(
'settings.customization.items.item_unit_updated'
),
})
}
if (response.data.errors) {
notificationStore.showNotification({
type: 'error',
message: err.response.data.errors[0],
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchItemUnits(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/units`, { params })
.then((response) => {
this.itemUnits = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchItemUnit(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/units/${id}`)
.then((response) => {
this.currentItemUnit = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteItemUnit(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/units/${id}`)
.then((response) => {
if (!response.data.error) {
let index = this.itemUnits.findIndex((unit) => unit.id === id)
this.itemUnits.splice(index, 1)
}
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t(
'settings.customization.items.deleted_message'
),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,144 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useMailDriverStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('mail-driver', {
state: () => ({
mailConfigData: null,
mail_driver: 'smtp',
mail_drivers: [],
basicMailConfig: {
mail_driver: '',
mail_host: '',
from_mail: '',
from_name: '',
},
mailgunConfig: {
mail_driver: '',
mail_mailgun_domain: '',
mail_mailgun_secret: '',
mail_mailgun_endpoint: '',
from_mail: '',
from_name: '',
},
sesConfig: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_ses_key: '',
mail_ses_secret: '',
mail_ses_region: '',
from_mail: '',
from_name: '',
},
smtpConfig: {
mail_driver: '',
mail_host: '',
mail_port: null,
mail_username: '',
mail_password: '',
mail_encryption: '',
from_mail: '',
from_name: '',
},
}),
actions: {
fetchMailDrivers() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/mail/drivers')
.then((response) => {
if (response.data) {
this.mail_drivers = response.data
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchMailConfig() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/mail/config')
.then((response) => {
if (response.data) {
this.mailConfigData = response.data
this.mail_driver = response.data.mail_driver
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateMailConfig(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/mail/config', data)
.then((response) => {
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('wizard.success.' + response.data.success),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('wizard.errors.' + response.data.error),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
sendTestMail(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/mail/test', data)
.then((response) => {
const notificationStore = useNotificationStore()
if (response.data.success) {
notificationStore.showNotification({
type: 'success',
message: global.t('general.send_mail_successfully'),
})
} else {
notificationStore.showNotification({
type: 'error',
message: global.t('validation.something_went_wrong'),
})
}
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

View File

@@ -1,289 +0,0 @@
import http from '@/scripts/http'
import { defineStore } from 'pinia'
import { useNotificationStore } from '@/scripts/stores/notification'
import { handleError } from '@/scripts/helpers/error-handling'
export const useMembersStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
const { global } = window.i18n
return defineStoreFunc('members', {
state: () => ({
roles: [],
users: [],
totalUsers: 0,
pendingInvitations: [],
currentUser: null,
selectAllField: false,
selectedUsers: [],
customerList: [],
userList: [],
userData: {
name: '',
email: '',
password: null,
phone: null,
companies: [],
},
}),
actions: {
resetUserData() {
this.userData = {
name: '',
email: '',
password: null,
phone: null,
role: null,
companies: [],
}
},
fetchUsers(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/members`, { params })
.then((response) => {
this.users = response.data.data
this.totalUsers = response.data.meta.total
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
fetchUser(id) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/members/${id}`)
.then((response) => {
this.userData = response.data.data
if (this.userData?.companies?.length) {
this.userData.companies.forEach((c, i) => {
this.userData.roles.forEach((r) => {
if (r.scope === c.id)
this.userData.companies[i].role = r.name
})
})
}
resolve(response)
})
.catch((err) => {
console.log(err)
handleError(err)
reject(err)
})
})
},
fetchRoles(state) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/roles`)
.then((response) => {
this.roles = response.data.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
addUser(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/members', data)
.then((response) => {
this.users.push(response.data)
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('members.created_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
updateUser(data) {
return new Promise((resolve, reject) => {
http
.put(`/api/v1/members/${data.id}`, data)
.then((response) => {
if (response) {
let pos = this.users.findIndex(
(user) => user.id === response.data.data.id
)
this.users[pos] = response.data.data
}
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('members.updated_message'),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteUser(id) {
const notificationStore = useNotificationStore()
return new Promise((resolve, reject) => {
http
.post(`/api/v1/members/delete`, { users: id.ids })
.then((response) => {
let index = this.users.findIndex((user) => user.id === id)
this.users.splice(index, 1)
notificationStore.showNotification({
type: 'success',
message: global.t('members.deleted_message', 1),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
deleteMultipleUsers() {
return new Promise((resolve, reject) => {
http
.post(`/api/v1/members/delete`, { users: this.selectedUsers })
.then((response) => {
this.selectedUsers.forEach((user) => {
let index = this.users.findIndex(
(_user) => _user.id === user.id
)
this.users.splice(index, 1)
})
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('members.deleted_message', 2),
})
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
searchUsers(params) {
return new Promise((resolve, reject) => {
http
.get(`/api/v1/search`, { params })
.then((response) => {
this.userList = response.data.users.data
this.customerList = response.data.customers.data
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
setSelectAllState(data) {
this.selectAllField = data
},
selectUser(data) {
this.selectedUsers = data
if (this.selectedUsers.length === this.users.length) {
this.selectAllField = true
} else {
this.selectAllField = false
}
},
selectAllUsers() {
if (this.selectedUsers.length === this.users.length) {
this.selectedUsers = []
this.selectAllField = false
} else {
let allUserIds = this.users.map((user) => user.id)
this.selectedUsers = allUserIds
this.selectAllField = true
}
},
fetchPendingInvitations() {
return new Promise((resolve, reject) => {
http
.get('/api/v1/company-invitations')
.then((response) => {
this.pendingInvitations = response.data.invitations
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
inviteMember(data) {
return new Promise((resolve, reject) => {
http
.post('/api/v1/company-invitations', data)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('members.invited_message'),
})
this.fetchPendingInvitations()
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
cancelInvitation(id) {
return new Promise((resolve, reject) => {
http
.delete(`/api/v1/company-invitations/${id}`)
.then((response) => {
const notificationStore = useNotificationStore()
notificationStore.showNotification({
type: 'success',
message: global.t('members.invitation_cancelled'),
})
this.pendingInvitations = this.pendingInvitations.filter(
(inv) => inv.id !== id
)
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
})
},
},
})()
}

Some files were not shown because too many files have changed in this diff Show More