mirror of
https://github.com/crater-invoice/crater.git
synced 2025-12-15 18:02:55 -05:00
v6 update
This commit is contained in:
493
resources/scripts/admin/admin-router.js
Normal file
493
resources/scripts/admin/admin-router.js
Normal file
@@ -0,0 +1,493 @@
|
||||
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 AccountSetting = () =>
|
||||
import('@/scripts/admin/views/settings/AccountSetting.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 MailConfig = () =>
|
||||
import('@/scripts/admin/views/settings/MailConfigSetting.vue')
|
||||
const FileDisk = () =>
|
||||
import('@/scripts/admin/views/settings/FileDiskSetting.vue')
|
||||
const Backup = () => import('@/scripts/admin/views/settings/BackupSetting.vue')
|
||||
const UpdateApp = () =>
|
||||
import('@/scripts/admin/views/settings/UpdateAppSetting.vue')
|
||||
const RolesSettings = () =>
|
||||
import('@/scripts/admin/views/settings/RolesSettings.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')
|
||||
|
||||
// Users
|
||||
const UserIndex = () => import('@/scripts/admin/views/users/Index.vue')
|
||||
const UserCreate = () => import('@/scripts/admin/views/users/Create.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')
|
||||
|
||||
export default [
|
||||
{
|
||||
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: '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,
|
||||
},
|
||||
|
||||
//settings
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'settings',
|
||||
component: SettingsIndex,
|
||||
children: [
|
||||
{
|
||||
path: 'account-settings',
|
||||
name: 'account.settings',
|
||||
component: AccountSetting,
|
||||
},
|
||||
{
|
||||
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: 'mailconfig',
|
||||
meta: { isOwner: true },
|
||||
component: MailConfig,
|
||||
},
|
||||
{
|
||||
path: 'file-disk',
|
||||
name: 'file-disk',
|
||||
meta: { isOwner: true },
|
||||
component: FileDisk,
|
||||
},
|
||||
{
|
||||
path: 'backup',
|
||||
name: 'backup',
|
||||
meta: { isOwner: true },
|
||||
component: Backup,
|
||||
},
|
||||
{
|
||||
path: 'update-app',
|
||||
name: 'updateapp',
|
||||
meta: { isOwner: true },
|
||||
component: UpdateApp,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// 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,
|
||||
},
|
||||
|
||||
// Users
|
||||
{
|
||||
path: 'users',
|
||||
name: 'users.index',
|
||||
meta: { isOwner: true },
|
||||
component: UserIndex,
|
||||
},
|
||||
{
|
||||
path: 'users/create',
|
||||
meta: { isOwner: true },
|
||||
name: 'users.create',
|
||||
component: UserCreate,
|
||||
},
|
||||
{
|
||||
path: 'users/:id/edit',
|
||||
name: 'users.edit',
|
||||
meta: { isOwner: true },
|
||||
component: UserCreate,
|
||||
},
|
||||
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '/:catchAll(.*)', component: NotFoundPage },
|
||||
]
|
||||
98
resources/scripts/admin/components/CopyInputField.vue
Normal file
98
resources/scripts/admin/components/CopyInputField.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
relative
|
||||
flex
|
||||
px-4
|
||||
py-2
|
||||
rounded-lg
|
||||
bg-opacity-40 bg-gray-300
|
||||
whitespace-nowrap
|
||||
flex-col
|
||||
mt-1
|
||||
"
|
||||
>
|
||||
<span
|
||||
ref="publicUrl"
|
||||
class="
|
||||
pr-10
|
||||
text-sm
|
||||
font-medium
|
||||
text-black
|
||||
truncate
|
||||
select-all select-color
|
||||
"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
<svg
|
||||
v-tooltip="{ content: 'Copy to Clipboard' }"
|
||||
class="
|
||||
absolute
|
||||
right-0
|
||||
h-full
|
||||
inset-y-0
|
||||
cursor-pointer
|
||||
focus:outline-none
|
||||
text-primary-500
|
||||
"
|
||||
width="37"
|
||||
viewBox="0 0 37 37"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click="copyUrl"
|
||||
>
|
||||
<rect width="37" height="37" rx="10" fill="currentColor" />
|
||||
<path
|
||||
d="M16 10C15.7348 10 15.4804 10.1054 15.2929 10.2929C15.1054 10.4804 15 10.7348 15 11C15 11.2652 15.1054 11.5196 15.2929 11.7071C15.4804 11.8946 15.7348 12 16 12H18C18.2652 12 18.5196 11.8946 18.7071 11.7071C18.8946 11.5196 19 11.2652 19 11C19 10.7348 18.8946 10.4804 18.7071 10.2929C18.5196 10.1054 18.2652 10 18 10H16Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M11 13C11 12.4696 11.2107 11.9609 11.5858 11.5858C11.9609 11.2107 12.4696 11 13 11C13 11.7956 13.3161 12.5587 13.8787 13.1213C14.4413 13.6839 15.2044 14 16 14H18C18.7956 14 19.5587 13.6839 20.1213 13.1213C20.6839 12.5587 21 11.7956 21 11C21.5304 11 22.0391 11.2107 22.4142 11.5858C22.7893 11.9609 23 12.4696 23 13V19H18.414L19.707 17.707C19.8892 17.5184 19.99 17.2658 19.9877 17.0036C19.9854 16.7414 19.8802 16.4906 19.6948 16.3052C19.5094 16.1198 19.2586 16.0146 18.9964 16.0123C18.7342 16.01 18.4816 16.1108 18.293 16.293L15.293 19.293C15.1055 19.4805 15.0002 19.7348 15.0002 20C15.0002 20.2652 15.1055 20.5195 15.293 20.707L18.293 23.707C18.4816 23.8892 18.7342 23.99 18.9964 23.9877C19.2586 23.9854 19.5094 23.8802 19.6948 23.6948C19.8802 23.5094 19.9854 23.2586 19.9877 22.9964C19.99 22.7342 19.8892 22.4816 19.707 22.293L18.414 21H23V24C23 24.5304 22.7893 25.0391 22.4142 25.4142C22.0391 25.7893 21.5304 26 21 26H13C12.4696 26 11.9609 25.7893 11.5858 25.4142C11.2107 25.0391 11 24.5304 11 24V13ZM23 19H25C25.2652 19 25.5196 19.1054 25.7071 19.2929C25.8946 19.4804 26 19.7348 26 20C26 20.2652 25.8946 20.5196 25.7071 20.7071C25.5196 20.8946 25.2652 21 25 21H23V19Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { ref } from 'vue'
|
||||
const notificationStore = useNotificationStore()
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const publicUrl = ref('')
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
token: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
function selectText(element) {
|
||||
let range
|
||||
if (document.selection) {
|
||||
// IE
|
||||
range = document.body.createTextRange()
|
||||
range.moveToElementText(element)
|
||||
range.select()
|
||||
} else if (window.getSelection) {
|
||||
range = document.createRange()
|
||||
range.selectNode(element)
|
||||
window.getSelection().removeAllRanges()
|
||||
window.getSelection().addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
selectText(publicUrl.value)
|
||||
document.execCommand('copy')
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
209
resources/scripts/admin/components/SelectNotePopup.vue
Normal file
209
resources/scripts/admin/components/SelectNotePopup.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<NoteModal />
|
||||
<div class="w-full">
|
||||
<Popover v-slot="{ isOpen }">
|
||||
<PopoverButton
|
||||
v-if="userStore.hasAbilities(abilities.VIEW_NOTE)"
|
||||
:class="isOpen ? '' : 'text-opacity-90'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
z-10
|
||||
font-medium
|
||||
text-primary-400
|
||||
focus:outline-none focus:border-none
|
||||
"
|
||||
@click="fetchInitialData"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PlusIcon"
|
||||
class="w-4 h-4 font-medium text-primary-400"
|
||||
/>
|
||||
{{ $t('general.insert_note') }}
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Note Select Popup -->
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
class="
|
||||
absolute
|
||||
z-20
|
||||
px-4
|
||||
mt-3
|
||||
sm:px-0
|
||||
w-screen
|
||||
max-w-full
|
||||
left-0
|
||||
top-3
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
shadow-lg
|
||||
ring-1 ring-black ring-opacity-5
|
||||
"
|
||||
>
|
||||
<div class="relative grid bg-white">
|
||||
<div class="relative p-4">
|
||||
<BaseInput
|
||||
v-model="textSearch"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
class="text-black"
|
||||
>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="filteredNotes.length > 0"
|
||||
class="relative flex flex-col overflow-auto list max-h-36"
|
||||
>
|
||||
<div
|
||||
v-for="(note, index) in filteredNotes"
|
||||
:key="index"
|
||||
tabindex="2"
|
||||
class="
|
||||
px-6
|
||||
py-4
|
||||
border-b border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:bg-gray-100 hover:cursor-pointer
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectNote(index, close)"
|
||||
>
|
||||
<div class="flex justify-between px-2">
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
leading-tight
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ note.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-center p-5 text-gray-400">
|
||||
<label class="text-base text-gray-500">
|
||||
{{ $t('general.no_note_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
type="button"
|
||||
class="
|
||||
h-10
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
py-3
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openNoteModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('settings.customization.notes.add_new_note') }}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
|
||||
const table = ref(null)
|
||||
const { t } = useI18n()
|
||||
const textSearch = ref(null)
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const noteStore = useNotesStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const filteredNotes = computed(() => {
|
||||
if (textSearch.value) {
|
||||
return noteStore.notes.filter(function (el) {
|
||||
return (
|
||||
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return noteStore.notes
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchInitialData() {
|
||||
await noteStore.fetchNotes({
|
||||
filter: {},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
type: props.type ? props.type : '',
|
||||
})
|
||||
}
|
||||
|
||||
function selectNote(data, close) {
|
||||
emit('select', { ...noteStore.notes[data] })
|
||||
|
||||
textSearch.value = null
|
||||
close()
|
||||
}
|
||||
|
||||
function openNoteModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.notes.add_note'),
|
||||
componentName: 'NoteModal',
|
||||
size: 'lg',
|
||||
data: props.type,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
197
resources/scripts/admin/components/charts/LineChart.vue
Normal file
197
resources/scripts/admin/components/charts/LineChart.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="graph-container h-[300px]">
|
||||
<canvas id="graph" ref="graph" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chart from 'chart.js'
|
||||
import { ref, reactive, computed, onMounted, watchEffect, inject } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
invoices: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
expenses: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
receipts: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
income: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
})
|
||||
|
||||
let myLineChart = null
|
||||
const graph = ref(null)
|
||||
const companyStore = useCompanyStore()
|
||||
const defaultCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.labels) {
|
||||
if (myLineChart) {
|
||||
myLineChart.reset()
|
||||
update()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
let context = graph.value.getContext('2d')
|
||||
let options = reactive({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
return utils.formatMoney(
|
||||
Math.round(tooltipItem.value * 100),
|
||||
defaultCurrency.value
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
})
|
||||
|
||||
let data = reactive({
|
||||
labels: props.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Sales',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(230, 254, 249)',
|
||||
borderColor: '#040405',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: '#040405',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: '#040405',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.invoices.map((invoice) => invoice / 100),
|
||||
},
|
||||
{
|
||||
label: 'Receipts',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(230, 254, 249)',
|
||||
borderColor: 'rgb(2, 201, 156)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgb(2, 201, 156)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgb(2, 201, 156)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.receipts.map((receipt) => receipt / 100),
|
||||
},
|
||||
{
|
||||
label: 'Expenses',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(245, 235, 242)',
|
||||
borderColor: 'rgb(255,0,0)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgb(255,0,0)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgb(255,0,0)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.expenses.map((expense) => expense / 100),
|
||||
},
|
||||
{
|
||||
label: 'Net Income',
|
||||
fill: false,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: 'rgba(236, 235, 249)',
|
||||
borderColor: 'rgba(88, 81, 216, 1)',
|
||||
borderCapStyle: 'butt',
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
borderJoinStyle: 'miter',
|
||||
pointBorderColor: 'rgba(88, 81, 216, 1)',
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 1,
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgba(88, 81, 216, 1)',
|
||||
pointHoverBorderColor: 'rgba(220,220,220,1)',
|
||||
pointHoverBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHitRadius: 10,
|
||||
data: props.income.map((_i) => _i / 100),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
myLineChart = new Chart(context, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options,
|
||||
})
|
||||
})
|
||||
|
||||
function update() {
|
||||
myLineChart.data.labels = props.labels
|
||||
myLineChart.data.datasets[0].data = props.invoices.map(
|
||||
(invoice) => invoice / 100
|
||||
)
|
||||
myLineChart.data.datasets[1].data = props.receipts.map(
|
||||
(receipt) => receipt / 100
|
||||
)
|
||||
myLineChart.data.datasets[2].data = props.expenses.map(
|
||||
(expense) => expense / 100
|
||||
)
|
||||
myLineChart.data.datasets[3].data = props.income.map((_i) => _i / 100)
|
||||
myLineChart.update({
|
||||
lazy: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<BaseCard>
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ $t('settings.exchange_rate.title') }}
|
||||
</h6>
|
||||
<p class="mt-2 text-sm leading-snug text-gray-500" style="max-width: 680px">
|
||||
{{
|
||||
$t('settings.exchange_rate.description', {
|
||||
currency: companyStore.selectedCompanyCurrency.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
||||
<form action="" @submit.prevent="submitBulkUpdate">
|
||||
<ValidateEach
|
||||
v-for="(c, i) in exchangeRateStore.bulkCurrencies"
|
||||
:key="i"
|
||||
:state="c"
|
||||
:rules="currencyArrayRules"
|
||||
>
|
||||
<template #default="{ v }">
|
||||
<BaseInputGroup
|
||||
class="my-5"
|
||||
:label="`${c.code} to ${companyStore.selectedCompanyCurrency.code}`"
|
||||
:error="
|
||||
v.exchange_rate.$error && v.exchange_rate.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="c.exchange_rate"
|
||||
:addon="`1 ${c.code} =`"
|
||||
:invalid="v.exchange_rate.$error"
|
||||
@input="v.exchange_rate.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<span class="text-gray-500 sm:text-sm">
|
||||
{{ companyStore.selectedCompanyCurrency.code }}
|
||||
</span>
|
||||
</template>
|
||||
</BaseInput>
|
||||
<span class="text-gray-400 text-xs mt-2 font-light">
|
||||
{{
|
||||
$t('settings.exchange_rate.exchange_help_text', {
|
||||
currency: c.code,
|
||||
baseCurrency: companyStore.selectedCompanyCurrency.code,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
</ValidateEach>
|
||||
<div
|
||||
slot="footer"
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
mt-4
|
||||
pt-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton :loading="isSaving" variant="primary" type="submit">
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { computed, ref } from '@vue/runtime-core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers, numeric, decimal } from '@vuelidate/validators'
|
||||
import { ValidateEach } from '@vuelidate/components'
|
||||
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
let isLoading = ref(false)
|
||||
|
||||
const currencyArrayRules = {
|
||||
exchange_rate: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
decimal: helpers.withMessage(t('validation.valid_exchange_rate'), decimal),
|
||||
},
|
||||
}
|
||||
const v = useVuelidate()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
async function submitBulkUpdate() {
|
||||
v.value.$touch()
|
||||
if (v.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
isSaving.value = true
|
||||
let data = exchangeRateStore.bulkCurrencies.map((_c) => {
|
||||
return {
|
||||
id: _c.id,
|
||||
exchange_rate: _c.exchange_rate,
|
||||
}
|
||||
})
|
||||
let res = await exchangeRateStore.updateBulkExchangeRate({ currencies: data })
|
||||
if (res.data.success) {
|
||||
emit('update', res.data.success)
|
||||
}
|
||||
isSaving.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp] && store[storeProp].customFields.length > 0 && !isLoading
|
||||
"
|
||||
>
|
||||
<BaseInputGrid :layout="gridLayout">
|
||||
<SingleField
|
||||
v-for="(field, index) in store[storeProp].customFields"
|
||||
:key="field.id"
|
||||
:custom-field-scope="customFieldScope"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
:index="index"
|
||||
:field="field"
|
||||
/>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import lodash from 'lodash'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { watch } from 'vue'
|
||||
import SingleField from './CreateCustomFieldsSingle.vue'
|
||||
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
gridLayout: {
|
||||
type: String,
|
||||
default: 'two-column',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
customFieldScope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
getInitialCustomFields()
|
||||
|
||||
function mergeExistingValues() {
|
||||
if (props.isEdit) {
|
||||
props.store[props.storeProp].fields.forEach((field) => {
|
||||
const existingIndex = props.store[props.storeProp].customFields.findIndex(
|
||||
(f) => f.id === field.custom_field_id
|
||||
)
|
||||
|
||||
if (existingIndex > -1) {
|
||||
let value = field.default_answer
|
||||
|
||||
if (value && field.custom_field.type === 'DateTime') {
|
||||
value = moment(field.default_answer, 'YYYY-MM-DD HH:mm:ss').format(
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
}
|
||||
|
||||
props.store[props.storeProp].customFields[existingIndex] = {
|
||||
...field,
|
||||
id: field.custom_field_id,
|
||||
value: value,
|
||||
label: field.custom_field.label,
|
||||
options: field.custom_field.options,
|
||||
is_required: field.custom_field.is_required,
|
||||
placeholder: field.custom_field.placeholder,
|
||||
order: field.custom_field.order,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function getInitialCustomFields() {
|
||||
const res = await customFieldStore.fetchCustomFields({
|
||||
type: props.type,
|
||||
limit: 'all',
|
||||
})
|
||||
|
||||
let data = res.data.data
|
||||
|
||||
data.map((d) => (d.value = d.default_answer))
|
||||
|
||||
props.store[props.storeProp].customFields = lodash.sortBy(
|
||||
data,
|
||||
(_cf) => _cf.order
|
||||
)
|
||||
|
||||
mergeExistingValues()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].fields,
|
||||
(val) => {
|
||||
mergeExistingValues()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<BaseInputGroup
|
||||
:label="field.label"
|
||||
:required="field.is_required ? true : false"
|
||||
:error="v$.value.$error && v$.value.$errors[0].$message"
|
||||
>
|
||||
<component
|
||||
:is="getTypeComponent"
|
||||
v-model="field.value"
|
||||
:options="field.options"
|
||||
:invalid="v$.value.$error"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineAsyncComponent, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { helpers, requiredIf } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
customFieldScope: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules = {
|
||||
value: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(props.field.is_required)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => props.field),
|
||||
{ $scope: props.customFieldScope }
|
||||
)
|
||||
|
||||
const getTypeComponent = computed(() => {
|
||||
if (props.field.type) {
|
||||
return defineAsyncComponent(() =>
|
||||
import(`./types/${props.field.type}Type.vue`)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseDatePicker v-model="date" enable-time />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: moment().format('YYYY-MM-DD hh:MM'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseDatePicker v-model="date" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import moment from 'moment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Date],
|
||||
default: moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<BaseMultiselect
|
||||
v-model="inputValue"
|
||||
:options="options"
|
||||
:label="label"
|
||||
:value-prop="valueProp"
|
||||
:object="object"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Object, Number],
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
valueProp: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
object: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="text" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="number" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="tel" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<BaseSwitch v-model="inputValue" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue === 1,
|
||||
set: (value) => {
|
||||
const intVal = value ? 1 : 0
|
||||
|
||||
emit('update:modelValue', intVal)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<BaseTextarea v-model="inputValue" :rows="rows" :name="inputName" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
rows: {
|
||||
type: String,
|
||||
default: '2',
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
default: 'description',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseTimePicker v-model="date" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import moment from 'moment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Date, Object],
|
||||
default: moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const date = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<BaseInput v-model="inputValue" type="url" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit customField -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_CUSTOM_FIELDS)"
|
||||
@click="editCustomField(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete customField -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_CUSTOM_FIELDS)"
|
||||
@click="removeCustomField(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editCustomField(id) {
|
||||
await customFieldStore.fetchCustomField(id)
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.custom_fields.edit_custom_field'),
|
||||
componentName: 'CustomFieldModal',
|
||||
size: 'sm',
|
||||
data: id,
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
async function removeCustomField(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.custom_fields.custom_field_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await customFieldStore.deleteCustomFields(id)
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="customerStore.isFetchingViewData">
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'customers.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Customer -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_CUSTOMER)"
|
||||
:to="`/admin/customers/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- View Customer -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'customers.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_CUSTOMER)
|
||||
"
|
||||
:to="`customers/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Customer -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_CUSTOMER)"
|
||||
@click="removeCustomer(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const customerStore = useCustomerStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
function removeCustomer(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('customers.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
customerStore.deleteCustomer({ ids: [id] }).then((response) => {
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'estimates.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else class="text-gray-500" name="DotsHorizontalIcon" />
|
||||
</template>
|
||||
|
||||
<!-- Copy PDF url -->
|
||||
<BaseDropdownItem
|
||||
v-if="route.name === 'estimates.view'"
|
||||
@click="copyPdfUrl"
|
||||
>
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Edit Estimate -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_ESTIMATE)"
|
||||
:to="`/admin/estimates/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_ESTIMATE)"
|
||||
@click="removeEstimate(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- View Estimate -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_ESTIMATE)
|
||||
"
|
||||
:to="`estimates/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Convert into Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)"
|
||||
@click="convertInToinvoice(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentTextIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.convert_to_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as sent -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsSent(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_sent') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Send Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
"
|
||||
@click="sendEstimate(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.send_estimate') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Resend Estimate -->
|
||||
<BaseDropdownItem v-if="canResendEstimate(row)" @click="sendEstimate(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.resend_estimate') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as Accepted -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'ACCEPTED' &&
|
||||
userStore.hasAbilities(abilities.EDIT_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsAccepted(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_accepted') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Mark as Rejected -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'REJECTED' &&
|
||||
userStore.hasAbilities(abilities.EDIT_ESTIMATE)
|
||||
"
|
||||
@click="onMarkAsRejected(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="XCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('estimates.mark_as_rejected') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const utils = inject('utils')
|
||||
|
||||
const estimateStore = useEstimateStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
async function removeEstimate(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
id = id
|
||||
if (res) {
|
||||
estimateStore.deleteEstimate({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.table && props.table.refresh()
|
||||
|
||||
if (res.data) {
|
||||
router.push('/admin/estimates')
|
||||
}
|
||||
estimateStore.$patch((state) => {
|
||||
state.selectedEstimates = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function convertInToinvoice(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_conversion'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
estimateStore.convertToInvoice(id).then((res) => {
|
||||
if (res.data) {
|
||||
router.push(`/admin/invoices/${res.data.data.id}/edit`)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsSent(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_sent'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsSent(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function canResendEstimate(row) {
|
||||
return (
|
||||
(row.status == 'SENT' || row.status == 'VIEWED') &&
|
||||
route.name !== 'estimates.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_ESTIMATE)
|
||||
)
|
||||
}
|
||||
|
||||
async function sendEstimate(estimate) {
|
||||
modalStore.openModal({
|
||||
title: t('estimates.send_estimate'),
|
||||
componentName: 'SendEstimateModal',
|
||||
id: estimate.id,
|
||||
data: estimate,
|
||||
variant: 'lg',
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsAccepted(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_accepted'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'ACCEPTED',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsAccepted(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsRejected(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('estimates.confirm_mark_as_rejected'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'REJECTED',
|
||||
}
|
||||
if (response) {
|
||||
estimateStore.markAsRejected(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/estimates/pdf/${props.row.unique_hash}`
|
||||
|
||||
let response = utils.copyTextToClipboard(pdfUrl)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
v-if="route.name === 'expenseCategorys.view'"
|
||||
variant="primary"
|
||||
>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit expenseCategory -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_EXPENSE)"
|
||||
@click="editExpenseCategory(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete expenseCategory -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||
@click="removeExpenseCategory(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCategoryStore } from '@/scripts/admin/stores/category'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const expenseCategoryStore = useCategoryStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editExpenseCategory(data) {
|
||||
expenseCategoryStore.fetchCategory(data)
|
||||
modalStore.openModal({
|
||||
title: t('settings.expense_category.edit_category'),
|
||||
componentName: 'CategoryModal',
|
||||
refreshData: props.loadData,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removeExpenseCategory(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.expense_category.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async () => {
|
||||
let response = await expenseCategoryStore.deleteCategory(id)
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'expenses.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit expense -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_EXPENSE)"
|
||||
:to="`/admin/expenses/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete expense -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||
@click="removeExpense(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const expenseStore = useExpenseStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeExpense(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('expenses.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
expenseStore.deleteExpense({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
261
resources/scripts/admin/components/dropdowns/InvoiceIndexDropdown.vue
Executable file
261
resources/scripts/admin/components/dropdowns/InvoiceIndexDropdown.vue
Executable file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'invoices.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Invoice -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_INVOICE)"
|
||||
:to="`/admin/invoices/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem v-show="row.allow_edit">
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Copy PDF url -->
|
||||
<BaseDropdownItem v-if="route.name === 'invoices.view'" @click="copyPdfUrl">
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- View Invoice -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'invoices.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_INVOICE)
|
||||
"
|
||||
:to="`/admin/invoices/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Send Invoice Mail -->
|
||||
<BaseDropdownItem v-if="canSendInvoice(row)" @click="sendInvoice(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Resend Invoice -->
|
||||
<BaseDropdownItem v-if="canReSendInvoice(row)" @click="sendInvoice(row)">
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.resend_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Record payment -->
|
||||
<router-link :to="`/admin/payments/${row.id}/create`">
|
||||
<BaseDropdownItem
|
||||
v-if="row.status == 'SENT' && route.name !== 'invoices.view'"
|
||||
>
|
||||
<BaseIcon
|
||||
name="CreditCardIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.record_payment') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Mark as sent Invoice -->
|
||||
<BaseDropdownItem v-if="canSendInvoice(row)" @click="onMarkAsSent(row.id)">
|
||||
<BaseIcon
|
||||
name="CheckCircleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Clone Invoice into new invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)"
|
||||
@click="cloneInvoiceData(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentTextIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('invoices.clone_invoice') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- Delete Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_INVOICE)"
|
||||
@click="removeInvoice(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
function canReSendInvoice(row) {
|
||||
return (
|
||||
(row.status == 'SENT' || row.status == 'VIEWED') &&
|
||||
userStore.hasAbilities(abilities.SEND_INVOICE)
|
||||
)
|
||||
}
|
||||
|
||||
function canSendInvoice(row) {
|
||||
return (
|
||||
row.status == 'DRAFT' &&
|
||||
route.name !== 'invoices.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_INVOICE)
|
||||
)
|
||||
}
|
||||
|
||||
async function removeInvoice(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
id = id
|
||||
if (res) {
|
||||
invoiceStore.deleteInvoice({ ids: [id] }).then((res) => {
|
||||
if (res.data.success) {
|
||||
router.push('/admin/invoices')
|
||||
props.table && props.table.refresh()
|
||||
|
||||
invoiceStore.$patch((state) => {
|
||||
state.selectedInvoices = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function cloneInvoiceData(data) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_clone'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
invoiceStore.cloneInvoice(data).then((res) => {
|
||||
router.push(`/admin/invoices/${res.data.data.id}/edit`)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onMarkAsSent(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.invoice_mark_as_sent'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'primary',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((response) => {
|
||||
const data = {
|
||||
id: id,
|
||||
status: 'SENT',
|
||||
}
|
||||
if (response) {
|
||||
invoiceStore.markAsSent(data).then((response) => {
|
||||
props.table && props.table.refresh()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function sendInvoice(invoice) {
|
||||
modalStore.openModal({
|
||||
title: t('invoices.send_invoice'),
|
||||
componentName: 'SendInvoiceModal',
|
||||
id: invoice.id,
|
||||
data: invoice,
|
||||
variant: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/invoices/pdf/${props.row.unique_hash}`
|
||||
|
||||
utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'items.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit item -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_ITEM)"
|
||||
:to="`/admin/items/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete item -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_ITEM)"
|
||||
@click="removeItem(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const itemStore = useItemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeItem(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('items.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
itemStore.deleteItem({ ids: [id] }).then((response) => {
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'notes.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit note -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
@click="editNote(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete note -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.MANAGE_NOTE)"
|
||||
@click="removeNote(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const noteStore = useNotesStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editNote(data) {
|
||||
noteStore.fetchNote(data)
|
||||
modalStore.openModal({
|
||||
title: t('settings.customization.notes.edit_note'),
|
||||
componentName: 'NoteModal',
|
||||
size: 'md',
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
function removeNote(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.customization.notes.note_confirm_delete'),
|
||||
yesLabel: t('general.yes'),
|
||||
noLabel: t('general.no'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async () => {
|
||||
let response = await noteStore.deleteNote(id)
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.deleted_message'),
|
||||
})
|
||||
} else {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('settings.customization.notes.already_in_use'),
|
||||
})
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="contentLoading">
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'payments.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Copy pdf url -->
|
||||
<BaseDropdown-item
|
||||
v-if="
|
||||
route.name === 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_PAYMENT)
|
||||
"
|
||||
class="rounded-md"
|
||||
@click="copyPdfUrl"
|
||||
>
|
||||
<BaseIcon
|
||||
name="LinkIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</BaseDropdown-item>
|
||||
|
||||
<!-- edit payment -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_PAYMENT)"
|
||||
:to="`/admin/payments/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- view payment -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_PAYMENT)
|
||||
"
|
||||
:to="`/admin/payments/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Send Estimate -->
|
||||
<BaseDropdownItem
|
||||
v-if="
|
||||
row.status !== 'SENT' &&
|
||||
route.name !== 'payments.view' &&
|
||||
userStore.hasAbilities(abilities.SEND_PAYMENT)
|
||||
"
|
||||
@click="sendPayment(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PaperAirplaneIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('payments.send_payment') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete payment -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_PAYMENT)"
|
||||
@click="removePayment(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
contentLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const paymentStore = usePaymentStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removePayment(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('payments.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await paymentStore.deletePayment({ ids: [id] })
|
||||
router.push(`/admin/payments`)
|
||||
props.table && props.table.refresh()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/payments/pdf/${props.row?.unique_hash}`
|
||||
|
||||
$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('general.copied_pdf_url_clipboard'),
|
||||
})
|
||||
}
|
||||
|
||||
async function sendPayment(payment) {
|
||||
modalStore.openModal({
|
||||
title: t('payments.send_payment'),
|
||||
componentName: 'SendPaymentModal',
|
||||
id: payment.id,
|
||||
data: payment,
|
||||
variant: 'lg',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'paymentModes.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit paymentMode -->
|
||||
<BaseDropdownItem @click="editPaymentMode(row.id)">
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete paymentMode -->
|
||||
<BaseDropdownItem @click="removePaymentMode(row.id)">
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const paymentStore = usePaymentStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function editPaymentMode(id) {
|
||||
paymentStore.fetchPaymentMode(id)
|
||||
modalStore.openModal({
|
||||
title: t('settings.payment_modes.edit_payment_mode'),
|
||||
componentName: 'PaymentModeModal',
|
||||
refreshData: props.loadData && props.loadData,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removePaymentMode(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.payment_modes.payment_mode_confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await paymentStore.deletePaymentMode(id)
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<BaseDropdown :content-loading="recurringInvoiceStore.isFetchingViewData">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
v-if="route.name === 'recurring-invoices.view'"
|
||||
variant="primary"
|
||||
>
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- Edit Recurring Invoice -->
|
||||
<router-link
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_RECURRING_INVOICE)"
|
||||
:to="`/admin/recurring-invoices/${row.id}/edit`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- View Recurring Invoice -->
|
||||
<router-link
|
||||
v-if="
|
||||
route.name !== 'recurring-invoices.view' &&
|
||||
userStore.hasAbilities(abilities.VIEW_RECURRING_INVOICE)
|
||||
"
|
||||
:to="`recurring-invoices/${row.id}/view`"
|
||||
>
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.view') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- Delete Recurring Invoice -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_RECURRING_INVOICE)"
|
||||
@click="removeMultipleRecurringInvoices(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { inject } from 'vue'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
|
||||
async function removeMultipleRecurringInvoices(id = null) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('invoices.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await recurringInvoiceStore
|
||||
.deleteMultipleRecurringInvoices(id)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
props.table && props.table.refresh()
|
||||
recurringInvoiceStore.$patch((state) => {
|
||||
state.selectedRecurringInvoices = []
|
||||
state.selectAllField = false
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('recurring_invoices.deleted_message', 2),
|
||||
})
|
||||
} else if (res.data.error) {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: res.data.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'roles.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit role -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.currentUser.is_owner"
|
||||
@click="editRole(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete role -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.currentUser.is_owner"
|
||||
@click="removeRole(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoleStore } from '@/scripts/admin/stores/role'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const roleStore = useRoleStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editRole(id) {
|
||||
Promise.all([
|
||||
await roleStore.fetchAbilities(),
|
||||
await roleStore.fetchRole(id),
|
||||
]).then(() => {
|
||||
modalStore.openModal({
|
||||
title: t('settings.roles.edit_role'),
|
||||
componentName: 'RolesModal',
|
||||
size: 'lg',
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function removeRole(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.roles.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
await roleStore.deleteRole(id).then((response) => {
|
||||
if (response.data) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'tax-types.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit tax-type -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.EDIT_TAX_TYPE)"
|
||||
@click="editTaxType(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete tax-type -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_TAX_TYPE)"
|
||||
@click="removeTaxType(row.id)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
async function editTaxType(id) {
|
||||
await taxTypeStore.fetchTaxType(id)
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.edit_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: props.loadData && props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
function removeTaxType(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('settings.tax_types.confirm_delete'),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res) {
|
||||
let response = await taxTypeStore.deleteTaxType(id)
|
||||
if (response.data.success) {
|
||||
props.loadData && props.loadData()
|
||||
return true
|
||||
}
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'users.view'" variant="primary">
|
||||
<BaseIcon name="DotsHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="DotsHorizontalIcon" class="h-5 text-gray-500" />
|
||||
</template>
|
||||
|
||||
<!-- edit user -->
|
||||
<router-link :to="`/admin/users/${row.id}/edit`">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="PencilIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.edit') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- delete user -->
|
||||
<BaseDropdownItem @click="removeUser(row.id)">
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUsersStore } from '@/scripts/admin/stores/users'
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
table: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
loadData: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const usersStore = useUsersStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function removeUser(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
message: t('users.confirm_delete', 1),
|
||||
yesLabel: t('general.ok'),
|
||||
noLabel: t('general.cancel'),
|
||||
variant: 'danger',
|
||||
size: 'lg',
|
||||
hideNoButton: false,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
usersStore.deleteUser({ ids: [id] }).then((res) => {
|
||||
if (res) {
|
||||
props.loadData && props.loadData()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
|
||||
<td colspan="5" class="p-0 text-left align-top">
|
||||
<table class="w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-5
|
||||
h-5
|
||||
mt-2
|
||||
text-gray-300
|
||||
cursor-move
|
||||
handle
|
||||
mr-2
|
||||
"
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
<BaseItemSelect
|
||||
type="Invoice"
|
||||
:item="itemData"
|
||||
:invalid="v$.name.$error"
|
||||
:invalid-description="v$.description.$error"
|
||||
:taxes="itemData.taxes"
|
||||
:index="index"
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
@search="searchVal"
|
||||
@select="onSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<BaseInput
|
||||
v-model="quantity"
|
||||
:invalid="v$.quantity.$error"
|
||||
:content-loading="loading"
|
||||
type="number"
|
||||
small
|
||||
min="0"
|
||||
step="any"
|
||||
@change="syncItemToStore()"
|
||||
@input="v$.quantity.$touch()"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-auto flex-fill bd-highlight">
|
||||
<div class="relative w-full">
|
||||
<BaseMoney
|
||||
:key="selectedCurrency"
|
||||
v-model="price"
|
||||
:invalid="v$.price.$error"
|
||||
:content-loading="loading"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex" style="width: 120px" role="group">
|
||||
<BaseInput
|
||||
v-model="discount"
|
||||
:invalid="v$.discount_val.$error"
|
||||
:content-loading="loading"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
:content-loading="loading"
|
||||
class="rounded-tr-md rounded-br-md !p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
itemData.discount_type == 'fixed'
|
||||
? currency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<BaseFormatMoney
|
||||
v-else
|
||||
:amount="total"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2">
|
||||
<BaseIcon
|
||||
v-if="showRemoveButton"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
name="TrashIcon"
|
||||
@click="store.removeItem(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="store[storeProp].tax_per_item === 'YES'">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<ItemTax
|
||||
v-for="(tax, index1) in itemData.taxes"
|
||||
v-else
|
||||
:key="tax.id"
|
||||
:index="index1"
|
||||
:item-index="index"
|
||||
:tax-data="tax"
|
||||
:taxes="itemData.taxes"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total="subtotal"
|
||||
:currency="currency"
|
||||
:update-items="syncItemToStore"
|
||||
:ability="abilities.CREATE_INVOICE"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/admin/stub/tax'
|
||||
import ItemTax from './CreateItemRowTax.vue'
|
||||
import { sumBy } from 'lodash'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
import {
|
||||
required,
|
||||
between,
|
||||
maxLength,
|
||||
helpers,
|
||||
minValue,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
invoiceItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove', 'itemValidate'])
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const itemStore = useItemStore()
|
||||
|
||||
let route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const quantity = computed({
|
||||
get: () => {
|
||||
return props.itemData.quantity
|
||||
},
|
||||
set: (newValue) => {
|
||||
updateItemAttribute('quantity', parseFloat(newValue))
|
||||
},
|
||||
})
|
||||
|
||||
const price = computed({
|
||||
get: () => {
|
||||
const price = props.itemData.price
|
||||
|
||||
if (parseFloat(price) > 0) {
|
||||
return price / 100
|
||||
}
|
||||
|
||||
return price
|
||||
},
|
||||
|
||||
set: (newValue) => {
|
||||
if (parseFloat(newValue) > 0) {
|
||||
let price = Math.round(newValue * 100)
|
||||
|
||||
updateItemAttribute('price', price)
|
||||
} else {
|
||||
updateItemAttribute('price', newValue)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
|
||||
|
||||
const discount = computed({
|
||||
get: () => {
|
||||
return props.itemData.discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
|
||||
} else {
|
||||
updateItemAttribute('discount_val', Math.round(newValue * 100))
|
||||
}
|
||||
|
||||
updateItemAttribute('discount', newValue)
|
||||
},
|
||||
})
|
||||
|
||||
const total = computed(() => {
|
||||
return subtotal.value - props.itemData.discount_val
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
const showRemoveButton = computed(() => {
|
||||
if (props.store[props.storeProp].items.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const totalSimpleTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalCompoundTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
quantity: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.qty_must_greater_than_zero'),
|
||||
minValue(0)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.amount_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
price: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.number_length_minvalue'),
|
||||
minValue(1)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.price_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
discount_val: {
|
||||
between: helpers.withMessage(
|
||||
t('validation.discount_maxlength'),
|
||||
between(
|
||||
0,
|
||||
computed(() => subtotal.value)
|
||||
)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.notes_maxlength'),
|
||||
maxLength(65000)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => props.store[props.storeProp].items[props.index]),
|
||||
{ $scope: props.itemValidationScope }
|
||||
)
|
||||
|
||||
//
|
||||
// if (
|
||||
// route.params.id &&
|
||||
// (props.store[props.storeProp].tax_per_item === 'YES' || 'NO')
|
||||
// ) {
|
||||
// if (props.store[props.storeProp].items[props.index].taxes === undefined) {
|
||||
// props.store.$patch((state) => {
|
||||
// state[props.storeProp].items[props.index].taxes = [
|
||||
// { ...TaxStub, id: Guid.raw() },
|
||||
// ]
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
function updateTax(data) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
|
||||
})
|
||||
|
||||
let lastTax = props.itemData.taxes[props.itemData.taxes.length - 1]
|
||||
|
||||
if (lastTax?.tax_type_id !== 0) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].taxes.push({
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function searchVal(val) {
|
||||
updateItemAttribute('name', val)
|
||||
}
|
||||
|
||||
function onSelectItem(itm) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].name = itm.name
|
||||
state[props.storeProp].items[props.index].price = itm.price
|
||||
state[props.storeProp].items[props.index].item_id = itm.id
|
||||
state[props.storeProp].items[props.index].description = itm.description
|
||||
|
||||
if (itm.unit) {
|
||||
state[props.storeProp].items[props.index].unit_name = itm.unit.name
|
||||
}
|
||||
|
||||
if (props.store[props.storeProp].tax_per_item === 'YES' && itm.taxes) {
|
||||
let index = 0
|
||||
|
||||
itm.taxes.forEach((tax) => {
|
||||
updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
|
||||
if (state[props.storeProp].exchange_rate) {
|
||||
state[props.storeProp].items[props.index].price /=
|
||||
state[props.storeProp].exchange_rate
|
||||
}
|
||||
})
|
||||
|
||||
itemStore.fetchItems()
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function selectFixed() {
|
||||
if (props.itemData.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute('discount_val', Math.round(props.itemData.discount * 100))
|
||||
updateItemAttribute('discount_type', 'fixed')
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute(
|
||||
'discount_val',
|
||||
(subtotal.value * props.itemData.discount) / 100
|
||||
)
|
||||
|
||||
updateItemAttribute('discount_type', 'percentage')
|
||||
}
|
||||
|
||||
function syncItemToStore() {
|
||||
let itemTaxes = props.store[props.storeProp]?.items[props.index]?.taxes
|
||||
|
||||
if (!itemTaxes) {
|
||||
itemTaxes = []
|
||||
}
|
||||
|
||||
let data = {
|
||||
...props.store[props.storeProp].items[props.index],
|
||||
index: props.index,
|
||||
total: total.value,
|
||||
sub_total: subtotal.value,
|
||||
totalSimpleTax: totalSimpleTax.value,
|
||||
totalCompoundTax: totalCompoundTax.value,
|
||||
totalTax: totalTax.value,
|
||||
tax: totalTax.value,
|
||||
taxes: [...itemTaxes],
|
||||
}
|
||||
|
||||
props.store.updateItem(data)
|
||||
}
|
||||
|
||||
function updateItemAttribute(attribute, value) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index][attribute] = value
|
||||
})
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center text-base" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('invoices.item.tax') }}
|
||||
</label>
|
||||
|
||||
<BaseMultiselect
|
||||
v-model="selectedTax"
|
||||
value-prop="id"
|
||||
:options="filteredTypes"
|
||||
:placeholder="$t('general.select_a_tax')"
|
||||
open-direction="top"
|
||||
track-by="name"
|
||||
searchable
|
||||
object
|
||||
label="name"
|
||||
@update:modelValue="(val) => onSelectTax(val)"
|
||||
>
|
||||
<template #singlelabel="{ value }">
|
||||
<div class="absolute left-3.5">
|
||||
{{ value.name }} - {{ value.percent }} %
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #option="{ option }">
|
||||
{{ option.name }} - {{ option.percent }} %
|
||||
</template>
|
||||
|
||||
<template v-if="userStore.hasAbilities(ability)" #action>
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
cursor-pointer
|
||||
py-2
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
|
||||
|
||||
<label
|
||||
class="ml-2 text-sm leading-none text-primary-400 cursor-pointer"
|
||||
>{{ $t('invoices.add_new_tax') }}</label
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
</BaseMultiselect>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-right" style="flex: 3">
|
||||
<BaseFormatMoney :amount="taxAmount" :currency="currency" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<BaseIcon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
name="TrashIcon"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
@click="removeTax(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject, reactive, watch } from 'vue'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
|
||||
const props = defineProps({
|
||||
ability: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
updateItems: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['remove', 'update'])
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const selectedTax = ref(null)
|
||||
const localTax = reactive({ ...props.taxData })
|
||||
const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredTypes = computed(() => {
|
||||
const clonedTypes = taxTypeStore.taxTypes.map((a) => ({ ...a }))
|
||||
|
||||
return clonedTypes.map((taxType) => {
|
||||
let found = props.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.disabled = true
|
||||
} else {
|
||||
taxType.disabled = false
|
||||
}
|
||||
|
||||
return taxType
|
||||
})
|
||||
})
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (localTax.compound_tax && props.total) {
|
||||
return ((props.total + props.totalTax) * localTax.percent) / 100
|
||||
}
|
||||
|
||||
if (props.total && localTax.percent) {
|
||||
return (props.total * localTax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.total,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.totalTax,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
// Set SelectedTax
|
||||
if (props.taxData.tax_type_id > 0) {
|
||||
selectedTax.value = taxTypeStore.taxTypes.find(
|
||||
(_type) => _type.id === props.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
updateRowTax()
|
||||
|
||||
function onSelectTax(val) {
|
||||
localTax.percent = val.percent
|
||||
localTax.tax_type_id = val.id
|
||||
localTax.compound_tax = val.compound_tax
|
||||
localTax.name = val.name
|
||||
|
||||
updateRowTax()
|
||||
}
|
||||
|
||||
function updateRowTax() {
|
||||
if (localTax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update', {
|
||||
index: props.index,
|
||||
item: {
|
||||
...localTax,
|
||||
amount: taxAmount.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openTaxModal() {
|
||||
let data = {
|
||||
itemIndex: props.itemIndex,
|
||||
taxIndex: props.index,
|
||||
}
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
data: data,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removeTax(index) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<table class="text-center item-table min-w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<thead class="bg-white border border-gray-200 border-solid">
|
||||
<tr>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pl-7">
|
||||
{{ $tc('items.item', 2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pr-10 column-heading">
|
||||
{{ $t('invoices.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="store[storeProp].items"
|
||||
item-key="id"
|
||||
tag="tbody"
|
||||
handle=".handle"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<Item
|
||||
:key="element.id"
|
||||
:index="index"
|
||||
:item-data="element"
|
||||
:loading="isLoading"
|
||||
:currency="defaultCurrency"
|
||||
:item-validation-scope="itemValidationScope"
|
||||
:invoice-items="store[storeProp].items"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-6
|
||||
py-3
|
||||
text-base
|
||||
border border-t-0 border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
text-primary-400
|
||||
hover:bg-primary-100
|
||||
"
|
||||
@click="store.addItem"
|
||||
>
|
||||
<BaseIcon name="PlusCircleIcon" class="mr-2" />
|
||||
{{ $t('general.add_new_item') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { computed } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import Item from './CreateItemRow.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String, null],
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="mb-6">
|
||||
<div
|
||||
class="z-20 text-sm font-semibold leading-5 text-primary-400 float-right"
|
||||
>
|
||||
<SelectNotePopup :type="type" @select="onSelectNote" />
|
||||
</div>
|
||||
<label class="text-gray-800 font-medium mb-4 text-sm">
|
||||
{{ $t('invoices.notes') }}
|
||||
</label>
|
||||
<BaseCustomInput
|
||||
v-model="store[storeProp].notes"
|
||||
:content-loading="store.isFetchingInitialSettings"
|
||||
:fields="fields"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import SelectNotePopup from '../SelectNotePopup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fields: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
function onSelectNote(data) {
|
||||
props.store[props.storeProp].notes = '' + data.notes
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
px-5
|
||||
py-4
|
||||
mt-6
|
||||
bg-white
|
||||
border border-gray-200 border-solid
|
||||
rounded
|
||||
md:min-w-[390px]
|
||||
min-w-[300px]
|
||||
lg:mt-7
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.sub_total') }}
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney
|
||||
:amount="store.getSubTotal"
|
||||
:currency="defaultCurrency"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tax in itemWiseTaxes"
|
||||
:key="tax.tax_type_id"
|
||||
class="flex items-center justify-between w-full"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-500 uppercase"
|
||||
>
|
||||
{{ tax.name }} - {{ tax.percent }}%
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].discount_per_item === 'NO' ||
|
||||
store[storeProp].discount_per_item === null
|
||||
"
|
||||
class="flex items-center justify-between w-full mt-2"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.discount') }}
|
||||
</label>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex" style="width: 140px" role="group">
|
||||
<BaseInput
|
||||
v-model="totalDiscount"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
class="rounded-tr-md rounded-br-md p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
store[storeProp].discount_type == 'fixed'
|
||||
? defaultCurrency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
>
|
||||
<Tax
|
||||
v-for="(tax, index) in taxes"
|
||||
:key="tax.id"
|
||||
:index="index"
|
||||
:tax="tax"
|
||||
:taxes="taxes"
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
@remove="removeTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
ref="taxModal"
|
||||
class="float-right pt-2 pb-4"
|
||||
>
|
||||
<SelectTaxPopup
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
:type="taxPopupType"
|
||||
@select:taxType="onSelectTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
w-full
|
||||
pt-2
|
||||
mt-5
|
||||
border-t border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label
|
||||
>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
text-lg
|
||||
uppercase
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="store.getTotal" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import Guid from 'guid'
|
||||
import Tax from './CreateTotalTaxes.vue'
|
||||
import TaxStub from '@/scripts/admin/stub/abilities'
|
||||
import SelectTaxPopup from './SelectTaxPopup.vue'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const taxModal = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
taxPopupType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const totalDiscount = computed({
|
||||
get: () => {
|
||||
return props.store[props.storeProp].discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
(props.store.getSubTotal * newValue) / 100
|
||||
)
|
||||
} else {
|
||||
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
|
||||
}
|
||||
props.store[props.storeProp].discount = newValue
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () => props.store[props.storeProp].taxes,
|
||||
set: (value) => {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const itemWiseTaxes = computed(() => {
|
||||
let taxes = []
|
||||
props.store[props.storeProp].items.forEach((item) => {
|
||||
if (item.taxes) {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return taxes
|
||||
})
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
function selectFixed() {
|
||||
if (props.store[props.storeProp].discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
props.store[props.storeProp].discount * 100
|
||||
)
|
||||
props.store[props.storeProp].discount_type = 'fixed'
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val =
|
||||
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
|
||||
props.store[props.storeProp].discount_type = 'percentage'
|
||||
}
|
||||
|
||||
function onSelectTax(selectedTax) {
|
||||
let amount = 0
|
||||
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
selectedTax.percent) /
|
||||
100
|
||||
)
|
||||
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
|
||||
amount = Math.round(
|
||||
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
|
||||
)
|
||||
}
|
||||
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount,
|
||||
}
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function updateTax(data) {
|
||||
const tax = props.store[props.storeProp].taxes.find(
|
||||
(tax) => tax.id === data.id
|
||||
)
|
||||
if (tax) {
|
||||
Object.assign(tax, { ...data })
|
||||
}
|
||||
}
|
||||
|
||||
function removeTax(id) {
|
||||
const index = props.store[props.storeProp].taxes.findIndex(
|
||||
(tax) => tax.id === id
|
||||
)
|
||||
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full mt-2 text-sm">
|
||||
<label class="font-semibold leading-5 text-gray-500 uppercase">
|
||||
{{ tax.name }} ({{ tax.percent }} %)
|
||||
</label>
|
||||
<label class="flex items-center justify-center text-lg text-black">
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="currency" />
|
||||
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="h-5 ml-2 cursor-pointer"
|
||||
@click="$emit('remove', tax.id)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch, inject, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (props.tax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
return Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
props.tax.percent) /
|
||||
100
|
||||
)
|
||||
}
|
||||
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
|
||||
return Math.round(
|
||||
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
|
||||
)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.store.getSubtotalWithDiscount) {
|
||||
updateTax()
|
||||
}
|
||||
if (props.store.getTotalSimpleTax) {
|
||||
updateTax()
|
||||
}
|
||||
})
|
||||
|
||||
function updateTax() {
|
||||
emit('update', {
|
||||
...props.tax,
|
||||
amount: taxAmount.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<BaseInputGroup
|
||||
v-if="store.showExchangeRate && selectedCurrency"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:label="$t('settings.exchange_rate.exchange_rate')"
|
||||
:error="v.exchange_rate.$error && v.exchange_rate.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<template #labelRight>
|
||||
<div v-if="hasActiveProvider && isEdit">
|
||||
<BaseIcon
|
||||
v-tooltip="{ content: 'Fetch Latest Exchange rate' }"
|
||||
name="RefreshIcon"
|
||||
:class="`h-4 w-4 text-primary-500 cursor-pointer outline-none ${
|
||||
isFetching
|
||||
? ' animate-spin rotate-180 cursor-not-allowed pointer-events-none '
|
||||
: ''
|
||||
}`"
|
||||
@click="getCurrenctExchangeRate(customerCurrency)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<BaseInput
|
||||
v-model="store[storeProp].exchange_rate"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:addon="`1 ${selectedCurrency.code} =`"
|
||||
:disabled="isFetching"
|
||||
@input="v.exchange_rate.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<span class="text-gray-500 sm:text-sm">
|
||||
{{ companyCurrency.code }}
|
||||
</span>
|
||||
</template>
|
||||
</BaseInput>
|
||||
<span class="text-gray-400 text-xs mt-2 font-light">
|
||||
{{
|
||||
$t('settings.exchange_rate.exchange_help_text', {
|
||||
currency: selectedCurrency.code,
|
||||
baseCurrency: companyCurrency.code,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
|
||||
const props = defineProps({
|
||||
v: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customerCurrency: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const globalStore = useGlobalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const hasActiveProvider = ref(false)
|
||||
let isFetching = ref(false)
|
||||
|
||||
globalStore.fetchCurrencies()
|
||||
|
||||
const companyCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
return globalStore.currencies.find(
|
||||
(c) => c.id === props.store[props.storeProp].currency_id
|
||||
)
|
||||
})
|
||||
|
||||
const isCurrencyDiffrent = computed(() => {
|
||||
return companyCurrency.value.id !== props.customerCurrency
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].customer,
|
||||
(v) => {
|
||||
setCustomerCurrency(v)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].currency_id,
|
||||
(v) => {
|
||||
onChangeCurrency(v)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => props.customerCurrency,
|
||||
(v) => {
|
||||
if (v && props.isEdit) {
|
||||
checkForActiveProvider(v)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function checkForActiveProvider() {
|
||||
if (isCurrencyDiffrent.value) {
|
||||
exchangeRateStore
|
||||
.checkForActiveProvider(props.customerCurrency)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
hasActiveProvider.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomerCurrency(v) {
|
||||
if (v) {
|
||||
props.store[props.storeProp].currency_id = v.currency.id
|
||||
} else {
|
||||
props.store[props.storeProp].currency_id = companyCurrency.value.id
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangeCurrency(v) {
|
||||
if (v !== companyCurrency.value.id) {
|
||||
if (!props.isEdit && v) {
|
||||
await getCurrenctExchangeRate(v)
|
||||
}
|
||||
|
||||
props.store.showExchangeRate = true
|
||||
} else {
|
||||
props.store.showExchangeRate = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrenctExchangeRate(v) {
|
||||
isFetching.value = true
|
||||
exchangeRateStore
|
||||
.getCurrentExchangeRate(v)
|
||||
.then((res) => {
|
||||
if (res.data && !res.data.error) {
|
||||
props.store[props.storeProp].exchange_rate = res.data.exchangeRate[0]
|
||||
} else {
|
||||
props.store[props.storeProp].exchange_rate = ''
|
||||
}
|
||||
isFetching.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.store.showExchangeRate = false
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<TaxationAddressModal @addTax="addSalesTax" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {} from '@/scripts/admin/stores/recurring-invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, watch, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import TaxationAddressModal from '@/scripts/admin/components/modal-components/TaxationAddressModal.vue'
|
||||
const SALES_TAX_US = 'Sales Tax'
|
||||
const SALES_TAX_MODULE = 'MODULE'
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const { t } = useI18n()
|
||||
import { isEqual, pick } from 'lodash'
|
||||
|
||||
const fetchingTax = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
customer: {
|
||||
type: [Object],
|
||||
default: null,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const isSalesTaxTypeBilling = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_address_type === 'billing'
|
||||
: companyStore.selectedCompanySettings.sales_tax_address_type === 'billing'
|
||||
})
|
||||
|
||||
const salesTaxEnabled = computed(() => {
|
||||
return companyStore.selectedCompanySettings.sales_tax_us_enabled === 'YES'
|
||||
})
|
||||
|
||||
const salesTaxCustomerLevel = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_type === 'customer_level'
|
||||
: companyStore.selectedCompanySettings.sales_tax_type === 'customer_level'
|
||||
})
|
||||
|
||||
const salesTaxCompanyLevel = computed(() => {
|
||||
return props.isEdit
|
||||
? props.store[props.storeProp].sales_tax_type === 'company_level'
|
||||
: companyStore.selectedCompanySettings.sales_tax_type === 'company_level'
|
||||
})
|
||||
|
||||
const addressData = computed(() => {
|
||||
if (salesTaxCustomerLevel.value && isAddressAvailable.value) {
|
||||
let address = isSalesTaxTypeBilling.value
|
||||
? props.customer.billing
|
||||
: props.customer.shipping
|
||||
return {
|
||||
address: pick(address, ['address_street_1', 'city', 'state', 'zip']),
|
||||
customer_id: props.customer.id,
|
||||
}
|
||||
} else if (salesTaxCompanyLevel.value && isAddressAvailable.value) {
|
||||
return {
|
||||
address: pick(address, ['address_street_1', 'city', 'state', 'zip']),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isAddressAvailable = computed(() => {
|
||||
if (salesTaxCustomerLevel.value) {
|
||||
let address = isSalesTaxTypeBilling.value
|
||||
? props.customer?.billing
|
||||
: props.customer?.shipping
|
||||
|
||||
return hasAddress(address)
|
||||
} else if (salesTaxCompanyLevel.value) {
|
||||
return hasAddress(companyStore.selectedCompany.address)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.customer,
|
||||
(v, o) => {
|
||||
if (v && o && salesTaxCustomerLevel.value) {
|
||||
// call if customer changed address
|
||||
isCustomerAddressChanged(v, o)
|
||||
return
|
||||
}
|
||||
if (!isAddressAvailable.value && salesTaxCustomerLevel.value && v) {
|
||||
setTimeout(() => {
|
||||
openAddressModal()
|
||||
}, 500)
|
||||
} else if (salesTaxCustomerLevel.value && v) {
|
||||
fetchSalesTax()
|
||||
} else if (salesTaxCustomerLevel.value && !v) {
|
||||
removeSalesTax()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Open modal for company address
|
||||
onMounted(() => {
|
||||
if (salesTaxCompanyLevel.value) {
|
||||
isAddressAvailable.value ? fetchSalesTax() : openAddressModal()
|
||||
}
|
||||
})
|
||||
|
||||
function hasAddress(address) {
|
||||
if (!address) return false
|
||||
|
||||
return (
|
||||
address.address_street_1 && address.city && address.state && address.zip
|
||||
)
|
||||
}
|
||||
|
||||
function isCustomerAddressChanged(newV, oldV) {
|
||||
const newData = isSalesTaxTypeBilling.value ? newV.billing : newV.shipping
|
||||
const oldData = isSalesTaxTypeBilling.value ? oldV.billing : oldV.shipping
|
||||
|
||||
const newAdd = pick(newData, ['address_street_1', 'city', 'state', 'zip'])
|
||||
const oldAdd = pick(oldData, ['address_street_1', 'city', 'state', 'zip'])
|
||||
!isEqual(newAdd, oldAdd) ? fetchSalesTax() : ''
|
||||
}
|
||||
|
||||
function openAddressModal() {
|
||||
if (!salesTaxEnabled.value) return
|
||||
let modalData = null
|
||||
let title = ''
|
||||
if (salesTaxCustomerLevel.value) {
|
||||
if (isSalesTaxTypeBilling.value) {
|
||||
modalData = props.customer?.billing
|
||||
title = t('settings.taxations.add_billing_address')
|
||||
} else {
|
||||
modalData = props.customer?.shipping
|
||||
title = t('settings.taxations.add_shipping_address')
|
||||
}
|
||||
} else {
|
||||
modalData = companyStore.selectedCompany.address
|
||||
title = t('settings.taxations.add_company_address')
|
||||
}
|
||||
|
||||
modalStore.openModal({
|
||||
title: title,
|
||||
content: t('settings.taxations.modal_description'),
|
||||
componentName: 'TaxationAddressModal',
|
||||
data: modalData,
|
||||
id: salesTaxCustomerLevel.value ? props.customer.id : '',
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchSalesTax() {
|
||||
if (!salesTaxEnabled.value) return
|
||||
|
||||
fetchingTax.value = true
|
||||
await taxTypeStore
|
||||
.fetchSalesTax(addressData.value)
|
||||
.then((res) => {
|
||||
addSalesTax(res.data.data)
|
||||
fetchingTax.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.data.error) {
|
||||
setTimeout(() => {
|
||||
openAddressModal()
|
||||
}, 500)
|
||||
}
|
||||
fetchingTax.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function addSalesTax(tax) {
|
||||
tax.tax_type_id = tax.id
|
||||
|
||||
const i = props.store[props.storeProp].taxes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
|
||||
if (i > -1) {
|
||||
Object.assign(props.store[props.storeProp].taxes[i], tax)
|
||||
} else {
|
||||
props.store[props.storeProp].taxes.push(tax)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSalesTax() {
|
||||
// remove from total taxes
|
||||
const i = props.store[props.storeProp].taxes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
i > -1 ? props.store[props.storeProp].taxes.splice(i, 1) : ''
|
||||
|
||||
// remove from tax-type list
|
||||
let pos = taxTypeStore.taxTypes.findIndex(
|
||||
(_t) => _t.name === SALES_TAX_US && _t.type === SALES_TAX_MODULE
|
||||
)
|
||||
|
||||
pos > -1 ? taxTypeStore.taxTypes.splice(pos, 1) : ''
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="w-full mt-4 tax-select">
|
||||
<Popover v-slot="{ isOpen }" class="relative">
|
||||
<PopoverButton
|
||||
:class="isOpen ? '' : 'text-opacity-90'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
text-sm
|
||||
font-medium
|
||||
text-primary-400
|
||||
focus:outline-none focus:border-none
|
||||
"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PlusIcon"
|
||||
class="w-4 h-4 font-medium text-primary-400"
|
||||
/>
|
||||
{{ $t('settings.tax_types.add_tax') }}
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Tax Select Popup -->
|
||||
<div class="relative w-full max-w-md px-4">
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
style="min-width: 350px; margin-left: 62px; top: -28px"
|
||||
class="absolute z-10 px-4 py-2 -translate-x-full sm:px-0"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
shadow-lg
|
||||
ring-1 ring-black ring-opacity-5
|
||||
"
|
||||
>
|
||||
<!-- Tax Search Input -->
|
||||
|
||||
<div class="relative bg-white">
|
||||
<div class="relative p-4">
|
||||
<BaseInput
|
||||
v-model="textSearch"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
class="text-black"
|
||||
>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<!-- List of Taxes -->
|
||||
<div
|
||||
v-if="filteredTaxType.length > 0"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
overflow-auto
|
||||
list
|
||||
max-h-36
|
||||
border-t border-gray-200
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(taxType, index) in filteredTaxType"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-gray-100 cursor-not-allowed opacity-50 pointer-events-none':
|
||||
taxes.find((val) => {
|
||||
return val.tax_type_id === taxType.id
|
||||
}),
|
||||
}"
|
||||
tabindex="2"
|
||||
class="
|
||||
px-6
|
||||
py-4
|
||||
border-b border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:bg-gray-100 hover:cursor-pointer
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectTaxType(taxType, close)"
|
||||
>
|
||||
<div class="flex justify-between px-2">
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
leading-tight
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.name }}
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.percent }} %
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex justify-center p-5 text-gray-400">
|
||||
<label class="text-base text-gray-500 cursor-pointer">
|
||||
{{ $t('general.no_tax_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add new Tax action -->
|
||||
<button
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_TAX_TYPE)"
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
h-10
|
||||
px-2
|
||||
py-3
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxTypeModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('estimates.add_new_tax') }}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref, inject, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select:taxType'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const textSearch = ref(null)
|
||||
|
||||
const filteredTaxType = computed(() => {
|
||||
if (textSearch.value) {
|
||||
return taxTypeStore.taxTypes.filter(function (el) {
|
||||
return (
|
||||
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return taxTypeStore.taxTypes
|
||||
}
|
||||
})
|
||||
|
||||
const taxes = computed(() => {
|
||||
return props.store[props.storeProp].taxes
|
||||
})
|
||||
|
||||
function selectTaxType(data, close) {
|
||||
emit('select:taxType', { ...data })
|
||||
close()
|
||||
}
|
||||
|
||||
function openTaxTypeModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: (data) => emit('select:taxType', data),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="flex text-gray-800 font-medium text-sm mb-2">
|
||||
{{ $t('general.select_template') }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</label>
|
||||
<BaseButton
|
||||
type="button"
|
||||
class="flex justify-center w-full text-sm lg:w-auto hover:bg-gray-200"
|
||||
variant="gray"
|
||||
@click="openTemplateModal"
|
||||
>
|
||||
<template #right="slotProps">
|
||||
<BaseIcon name="PencilIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ store[storeProp].template_name }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function openTemplateModal() {
|
||||
modalStore.openModal({
|
||||
title: t('general.choose_template'),
|
||||
componentName: 'SelectTemplate',
|
||||
data: {
|
||||
templates: props.store.templates,
|
||||
store: props.store,
|
||||
storeProp: props.storeProp,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="onCancel" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="createNewBackup">
|
||||
<div class="p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.backup.select_backup_type')"
|
||||
:error="
|
||||
v$.currentBackupData.option.$error &&
|
||||
v$.currentBackupData.option.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.option"
|
||||
:options="options"
|
||||
:can-deselect="false"
|
||||
:placeholder="$t('settings.backup.select_backup_type')"
|
||||
searchable
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.select_disk')"
|
||||
:error="
|
||||
v$.currentBackupData.selected_disk.$error &&
|
||||
v$.currentBackupData.selected_disk.$errors[0].$message
|
||||
"
|
||||
horizontal
|
||||
required
|
||||
class="py-2"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="backupStore.currentBackupData.selected_disk"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="getDisksOptions"
|
||||
:searchable="true"
|
||||
:allow-empty="false"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:placeholder="$t('settings.disk.select_disk')"
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isCreateLoading"
|
||||
:disabled="isCreateLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isCreateLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.create') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBackupStore } from '@/scripts/admin/stores/backup'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
let table = ref(null)
|
||||
let isSaving = ref(false)
|
||||
let isCreateLoading = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
const options = reactive(['full', 'only-db', 'only-files'])
|
||||
|
||||
const backupStore = useBackupStore()
|
||||
const modalStore = useModalStore()
|
||||
const diskStore = useDiskStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'BackupModal'
|
||||
})
|
||||
|
||||
const getDisksOptions = computed(() => {
|
||||
return diskStore.disks.map((disk) => {
|
||||
return {
|
||||
...disk,
|
||||
name: disk.name + ' — ' + '[' + disk.driver + ']',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentBackupData: {
|
||||
option: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_disk: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => backupStore)
|
||||
)
|
||||
|
||||
async function createNewBackup() {
|
||||
v$.value.currentBackupData.$touch()
|
||||
if (v$.value.currentBackupData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
option: backupStore.currentBackupData.option,
|
||||
file_disk_id: backupStore.currentBackupData.selected_disk.id,
|
||||
}
|
||||
try {
|
||||
isCreateLoading.value = true
|
||||
let res = await backupStore.createBackup(data)
|
||||
if (res.data) {
|
||||
isCreateLoading.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
modalStore.closeModal()
|
||||
}
|
||||
} catch (e) {
|
||||
isCreateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isFetchingInitialData.value = true
|
||||
let res = await diskStore.fetchDisks({ limit: 'all' })
|
||||
backupStore.currentBackupData.selected_disk = res.data.data[0]
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
backupStore.$reset()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCategoryModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCategoryData">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.category')"
|
||||
:error="
|
||||
v$.currentCategory.name.$error &&
|
||||
v$.currentCategory.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="categoryStore.currentCategory.name"
|
||||
:invalid="v$.currentCategory.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentCategory.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.description')"
|
||||
:error="
|
||||
v$.currentCategory.description.$error &&
|
||||
v$.currentCategory.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="categoryStore.currentCategory.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentCategory.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeCategoryModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ categoryStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCategoryStore } from '@/scripts/admin/stores/category'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const categoryStore = useCategoryStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCategory: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => categoryStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CategoryModal'
|
||||
})
|
||||
|
||||
async function submitCategoryData() {
|
||||
v$.value.currentCategory.$touch()
|
||||
|
||||
if (v$.value.currentCategory.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const action = categoryStore.isEdit
|
||||
? categoryStore.updateCategory
|
||||
: categoryStore.addCategory
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(categoryStore.currentCategory)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCategoryModal()
|
||||
}
|
||||
|
||||
function closeCategoryModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
categoryStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal" @open="getInitials">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCompanyModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 mb-16 sm:p-6 space-y-4">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.company_logo')"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isFetchingInitialData">
|
||||
<BaseContentPlaceholdersBox :rounded="true" class="w-full h-24" />
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<BaseFileUploader
|
||||
:preview-image="previewLogo"
|
||||
base64
|
||||
@remove="onFileInputRemove"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.company_info.company_name')"
|
||||
:error="
|
||||
v$.newCompanyForm.name.$error &&
|
||||
v$.newCompanyForm.name.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="newCompanyForm.name"
|
||||
:invalid="v$.newCompanyForm.name.$error"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.newCompanyForm.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$tc('settings.company_info.country')"
|
||||
:error="
|
||||
v$.newCompanyForm.address.country_id.$error &&
|
||||
v$.newCompanyForm.address.country_id.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.address.country_id"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="name"
|
||||
:invalid="v$.newCompanyForm.address.country_id.$error"
|
||||
:options="globalStore.countries"
|
||||
value-prop="id"
|
||||
:can-deselect="true"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.currency')"
|
||||
:error="
|
||||
v$.newCompanyForm.currency.$error &&
|
||||
v$.newCompanyForm.currency.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:help-text="$t('wizard.currency_set_alert')"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.currency"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.currencies"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:searchable="true"
|
||||
track-by="name"
|
||||
:placeholder="$tc('settings.currencies.select_currency')"
|
||||
:invalid="v$.newCompanyForm.currency.$error"
|
||||
class="w-full"
|
||||
>
|
||||
</BaseMultiselect>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
let previewLogo = ref(null)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let companyLogoFileBlob = ref(null)
|
||||
let companyLogoName = ref(null)
|
||||
|
||||
const newCompanyForm = reactive({
|
||||
name: null,
|
||||
currency: '',
|
||||
address: {
|
||||
country_id: null,
|
||||
},
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
newCompanyForm: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
address: {
|
||||
country_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currency: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { newCompanyForm })
|
||||
|
||||
async function getInitials() {
|
||||
isFetchingInitialData.value = true
|
||||
await globalStore.fetchCurrencies()
|
||||
await globalStore.fetchCountries()
|
||||
|
||||
newCompanyForm.currency = companyStore.selectedCompanyCurrency.id
|
||||
newCompanyForm.address.country_id =
|
||||
companyStore.selectedCompany.address.country_id
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onFileInputChange(fileName, file) {
|
||||
companyLogoName.value = fileName
|
||||
companyLogoFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove() {
|
||||
companyLogoName.value = null
|
||||
companyLogoFileBlob.value = null
|
||||
}
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.newCompanyForm.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const res = await companyStore.addNewCompany(newCompanyForm)
|
||||
if (res.data.data) {
|
||||
await companyStore.setSelectedCompany(res.data.data)
|
||||
if (companyLogoFileBlob && companyLogoFileBlob.value) {
|
||||
let logoData = new FormData()
|
||||
|
||||
logoData.append(
|
||||
'company_logo',
|
||||
JSON.stringify({
|
||||
name: companyLogoName.value,
|
||||
data: companyLogoFileBlob.value,
|
||||
})
|
||||
)
|
||||
|
||||
await companyStore.updateCompanyLogo(logoData)
|
||||
router.push('/admin/dashboard')
|
||||
}
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
closeCompanyModal()
|
||||
}
|
||||
isSaving.value = false
|
||||
} catch {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
newCompanyForm.name = ''
|
||||
newCompanyForm.currency = ''
|
||||
newCompanyForm.address.country_id = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,665 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeCustomerModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomerModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitCustomerData">
|
||||
<div class="px-6 pb-3">
|
||||
<BaseTabGroup>
|
||||
<BaseTab :title="$t('customers.basic_info')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.display_name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.name"
|
||||
type="text"
|
||||
name="name"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.currencies.currency')"
|
||||
required
|
||||
:error="
|
||||
v$.currency_id.$error && v$.currency_id.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.currency_id"
|
||||
:options="globalStore.currencies"
|
||||
value-prop="id"
|
||||
searchable
|
||||
:placeholder="$t('customers.select_currency')"
|
||||
:max-height="200"
|
||||
class="mt-1 md:mt-0"
|
||||
track-by="name"
|
||||
:invalid="v$.currency_id.$error"
|
||||
label="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.primary_contact_name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.contact_name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('login.email')"
|
||||
:error="v$.email.$error && v$.email.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.email"
|
||||
type="text"
|
||||
name="email"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.email.$error"
|
||||
@input="v$.email.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.prefix')"
|
||||
:error="v$.prefix.$error && v$.prefix.$errors[0].$message"
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.prefix"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="name"
|
||||
class=""
|
||||
:invalid="v$.prefix.$error"
|
||||
@input="v$.prefix.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.website')"
|
||||
:error="v$.website.$error && v$.website.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.website"
|
||||
type="url"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.website.$error"
|
||||
@input="v$.website.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.portal_access')">
|
||||
<BaseInputGrid class="col-span-5 lg:col-span-4">
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ $t('customers.portal_access_text') }}
|
||||
</p>
|
||||
|
||||
<BaseSwitch
|
||||
v-model="customerStore.currentCustomer.enable_portal"
|
||||
class="mt-1 flex"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('customers.portal_access_url')"
|
||||
class="md:col-span-2"
|
||||
:help-text="$t('customers.portal_access_url_help')"
|
||||
>
|
||||
<CopyInputField :token="getCustomerPortalUrl" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="v$.password.$error && v$.password.$errors[0].$message"
|
||||
:label="$t('customers.password')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.password"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
:invalid="v$.password.$error"
|
||||
@input="v$.password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowPassword"
|
||||
name="EyeOffIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
/> </template
|
||||
></BaseInput>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
v-if="customerStore.currentCustomer.enable_portal"
|
||||
:error="
|
||||
v$.confirm_password.$error &&
|
||||
v$.confirm_password.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="Confirm Password"
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.confirm_password"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:type="isShowConfirmPassword ? 'text' : 'password'"
|
||||
name="confirm_password"
|
||||
:invalid="v$.confirm_password.$error"
|
||||
@input="v$.confirm_password.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<BaseIcon
|
||||
v-if="isShowConfirmPassword"
|
||||
name="EyeOffIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowConfirmPassword = !isShowConfirmPassword"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="EyeIcon"
|
||||
class="w-5 h-5 mr-1 text-gray-500 cursor-pointer"
|
||||
@click="isShowConfirmPassword = !isShowConfirmPassword"
|
||||
/> </template
|
||||
></BaseInput>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.billing_address')" class="!mt-2">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.billing.country_id"
|
||||
:options="globalStore.countries"
|
||||
searchable
|
||||
:show-labels="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
:allow-empty="false"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.state"
|
||||
type="text"
|
||||
name="billingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.city"
|
||||
type="text"
|
||||
name="billingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.billing.address_street_1.$error &&
|
||||
v$.billing.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.billing.address_street_1.$error"
|
||||
@input="v$.billing.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.billing.address_street_2.$error &&
|
||||
v$.billing.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.billing.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.billing.address_street_2.$error"
|
||||
@input="v$.billing.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.billing.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.billing.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
|
||||
<BaseTab :title="$t('customers.shipping_address')" class="!mt-2">
|
||||
<div class="grid md:grid-cols-12">
|
||||
<div class="flex justify-end col-span-12">
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="xs"
|
||||
@click="copyAddress(true)"
|
||||
>
|
||||
{{ $t('customers.copy_billing_address') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup :label="$t('customers.name')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.name"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.country')">
|
||||
<BaseMultiselect
|
||||
v-model="customerStore.currentCustomer.shipping.country_id"
|
||||
:options="globalStore.countries"
|
||||
:searchable="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:placeholder="$t('general.select_country')"
|
||||
track-by="name"
|
||||
class="mt-1 md:mt-0"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.state')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.state"
|
||||
type="text"
|
||||
name="shippingState"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.city')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.city"
|
||||
type="text"
|
||||
name="shippingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('customers.address')"
|
||||
:error="
|
||||
v$.shipping.address_street_1.$error &&
|
||||
v$.shipping.address_street_1.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_1
|
||||
"
|
||||
:placeholder="$t('general.street_1')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_1.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:error="
|
||||
v$.shipping.address_street_2.$error &&
|
||||
v$.shipping.address_street_2.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="
|
||||
customerStore.currentCustomer.shipping.address_street_2
|
||||
"
|
||||
:placeholder="$t('general.street_2')"
|
||||
rows="2"
|
||||
cols="50"
|
||||
:invalid="v$.shipping.address_street_1.$error"
|
||||
@input="v$.shipping.address_street_2.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.phone')">
|
||||
<BaseInput
|
||||
v-model.trim="customerStore.currentCustomer.shipping.phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('customers.zip_code')">
|
||||
<BaseInput
|
||||
v-model="customerStore.currentCustomer.shipping.zip"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</BaseTab>
|
||||
</BaseTabGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomerModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isLoading" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
email,
|
||||
alpha,
|
||||
url,
|
||||
helpers,
|
||||
requiredIf,
|
||||
sameAs,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import CopyInputField from '@/scripts/admin/components/CopyInputField.vue'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
|
||||
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const customerStore = useCustomerStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const isEdit = ref(false)
|
||||
const isLoading = ref(false)
|
||||
let isShowPassword = ref(false)
|
||||
let isShowConfirmPassword = ref(false)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'CustomerModal'
|
||||
)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
currency_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
password: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(
|
||||
customerStore.currentCustomer.enable_portal == true &&
|
||||
!customerStore.currentCustomer.password_added
|
||||
)
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.password_min_length', { count: 8 }),
|
||||
minLength(8)
|
||||
),
|
||||
},
|
||||
confirm_password: {
|
||||
sameAsPassword: helpers.withMessage(
|
||||
t('validation.password_incorrect'),
|
||||
sameAs(customerStore.currentCustomer.password)
|
||||
),
|
||||
},
|
||||
email: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(customerStore.currentCustomer.enable_portal == true)
|
||||
),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
prefix: {
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
website: {
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
|
||||
billing: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
shipping: {
|
||||
address_street_1: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
address_street_2: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.address_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customerStore.currentCustomer)
|
||||
)
|
||||
|
||||
const getCustomerPortalUrl = computed(() => {
|
||||
return `${window.location.origin}/${companyStore.selectedCompany.slug}/customer/login`
|
||||
})
|
||||
|
||||
function copyAddress() {
|
||||
customerStore.copyAddress()
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
if (!customerStore.isEdit) {
|
||||
customerStore.currentCustomer.currency_id =
|
||||
companyStore.selectedCompanyCurrency.id
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomerData() {
|
||||
if (customerStore.currentCustomer.email === '') {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('settings.notification.please_enter_email'),
|
||||
})
|
||||
}
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
let data = {
|
||||
...customerStore.currentCustomer,
|
||||
}
|
||||
|
||||
try {
|
||||
let response = null
|
||||
if (customerStore.isEdit) {
|
||||
response = await customerStore.updateCustomer(data)
|
||||
} else {
|
||||
response = await customerStore.addCustomer(data)
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
isLoading.value = false
|
||||
// Automatically create newly created customer
|
||||
if (route.name === 'invoices.create' || route.name === 'invoices.edit') {
|
||||
invoiceStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
if (
|
||||
route.name === 'estimates.create' ||
|
||||
route.name === 'estimates.edit'
|
||||
) {
|
||||
estimateStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
if (
|
||||
route.name === 'recurring-invoices.create' ||
|
||||
route.name === 'recurring-invoices.edit'
|
||||
) {
|
||||
recurringInvoiceStore.selectCustomer(response.data.data.id)
|
||||
}
|
||||
closeCustomerModal()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomerModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
customerStore.resetCurrentCustomer()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="px-6 pt-6">
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ modalStore.title }}
|
||||
</h6>
|
||||
<p
|
||||
class="mt-2 text-sm leading-snug text-gray-500"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
{{
|
||||
$t('settings.company_info.delete_company_modal_desc', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 sm:p-6 space-y-4">
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.company_info.delete_company_modal_label', {
|
||||
company: companyStore.selectedCompany.name,
|
||||
})
|
||||
"
|
||||
:error="
|
||||
v$.formData.name.$error && v$.formData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.name"
|
||||
:invalid="v$.formData.name.$error"
|
||||
@input="v$.formData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-gray-50 border-modal-bg">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
outline
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isDeleting"
|
||||
name="TrashIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers, sameAs } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
let isDeleting = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
id: companyStore.selectedCompany.id,
|
||||
name: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'DeleteCompanyModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
sameAsName: helpers.withMessage(
|
||||
t('validation.company_name_not_same'),
|
||||
sameAs(companyStore.selectedCompany.name)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
{ formData },
|
||||
{
|
||||
$scope: false,
|
||||
}
|
||||
)
|
||||
|
||||
async function submitCompanyData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
const company = companyStore.companies[0]
|
||||
|
||||
isDeleting.value = true
|
||||
try {
|
||||
const res = await companyStore.deleteCompany(formData)
|
||||
console.log(res.data.success)
|
||||
if (res.data.success) {
|
||||
closeCompanyModal()
|
||||
await companyStore.setSelectedCompany(company)
|
||||
router.push('/admin/dashboard')
|
||||
await globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
}
|
||||
isDeleting.value = false
|
||||
} catch {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm() {
|
||||
formData.id = null
|
||||
formData.name = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive">
|
||||
<ExchangeRateBulkUpdate @update="closeModal()" />
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ExchangeRateBulkUpdate from '@/scripts/admin/components/currency-exchange-rate/ExchangeRateBulkUpdate.vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateBulkUpdateModal'
|
||||
)
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,482 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeExchangeRateModal"
|
||||
@open="fetchInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeExchangeRateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitExchangeRate">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.exchange_rate.driver')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
:error="
|
||||
v$.currentExchangeRate.driver.$error &&
|
||||
v$.currentExchangeRate.driver.$errors[0].$message
|
||||
"
|
||||
:help-text="driverSite"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.driver"
|
||||
:options="driversLists"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
:can-deselect="true"
|
||||
label="key"
|
||||
:searchable="true"
|
||||
:invalid="v$.currentExchangeRate.driver.$error"
|
||||
@update:modelValue="resetCurrency"
|
||||
@input="v$.currentExchangeRate.driver.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
v-if="isCurrencyConverter"
|
||||
required
|
||||
:label="$t('settings.exchange_rate.server')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.type.$error &&
|
||||
v$.currencyConverter.type.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currencyConverter.type"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="value"
|
||||
searchable
|
||||
:options="serverOptions"
|
||||
:invalid="v$.currencyConverter.type.$error"
|
||||
label="value"
|
||||
@update:modelValue="resetCurrency"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.exchange_rate.key')"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.key.$error &&
|
||||
v$.currentExchangeRate.key.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currentExchangeRate.key"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="text"
|
||||
name="key"
|
||||
:loading="isFetchingCurrencies"
|
||||
loading-position="right"
|
||||
:invalid="v$.currentExchangeRate.key.$error"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="exchangeRateStore.supportedCurrencies.length"
|
||||
:label="$t('settings.exchange_rate.currency')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currentExchangeRate.currencies.$error &&
|
||||
v$.currentExchangeRate.currencies.$errors[0].$message
|
||||
"
|
||||
:help-text="$t('settings.exchange_rate.currency_help_text')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="exchangeRateStore.currentExchangeRate.currencies"
|
||||
:content-loading="isFetchingInitialData"
|
||||
value-prop="code"
|
||||
mode="tags"
|
||||
searchable
|
||||
:options="exchangeRateStore.supportedCurrencies"
|
||||
:invalid="v$.currentExchangeRate.currencies.$error"
|
||||
label="code"
|
||||
track-by="code"
|
||||
@input="v$.currentExchangeRate.currencies.$touch()"
|
||||
openDirection="top"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<!-- For Currency Converter -->
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDedicatedServer"
|
||||
:label="$t('settings.exchange_rate.url')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:error="
|
||||
v$.currencyConverter.url.$error &&
|
||||
v$.currencyConverter.url.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="exchangeRateStore.currencyConverter.url"
|
||||
:content-loading="isFetchingInitialData"
|
||||
type="url"
|
||||
:invalid="v$.currencyConverter.url.$error"
|
||||
@input="v$.currencyConverter.url.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseSwitch
|
||||
v-model="exchangeRateStore.currentExchangeRate.active"
|
||||
class="flex"
|
||||
:label-right="$t('settings.exchange_rate.active')"
|
||||
/>
|
||||
</BaseInputGrid>
|
||||
|
||||
<BaseInfoAlert
|
||||
v-if="
|
||||
currenciesAlredayInUsed.length &&
|
||||
exchangeRateStore.currentExchangeRate.active
|
||||
"
|
||||
class="mt-5"
|
||||
:title="$t('settings.exchange_rate.currency_in_used')"
|
||||
:lists="[currenciesAlredayInUsed.toString()]"
|
||||
:actions="['Remove']"
|
||||
@hide="dismiss"
|
||||
@Remove="removeUsedSelectedCurrencies"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
:disabled="isSaving"
|
||||
@click="closeExchangeRateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving || isFetchingCurrencies"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
exchangeRateStore.isEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
helpers,
|
||||
requiredIf,
|
||||
url,
|
||||
} from '@vuelidate/validators'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
let isFetchingCurrencies = ref(false)
|
||||
let currenciesAlredayInUsed = ref([])
|
||||
let currenctPorivderOldCurrencies = ref([])
|
||||
const modalStore = useModalStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
|
||||
let serverOptions = ref([])
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentExchangeRate: {
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
currencies: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currencyConverter: {
|
||||
type: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isCurrencyConverter)
|
||||
),
|
||||
},
|
||||
url: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(isDedicatedServer)
|
||||
),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
const driversLists = computed(() => {
|
||||
return exchangeRateStore.drivers.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
key: t(item.key),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return (
|
||||
modalStore.active &&
|
||||
modalStore.componentName === 'ExchangeRateProviderModal'
|
||||
)
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const isCurrencyConverter = computed(() => {
|
||||
return exchangeRateStore.currentExchangeRate.driver === 'currency_converter'
|
||||
})
|
||||
|
||||
const isDedicatedServer = computed(() => {
|
||||
return (
|
||||
exchangeRateStore.currencyConverter &&
|
||||
exchangeRateStore.currencyConverter.type === 'DEDICATED'
|
||||
)
|
||||
})
|
||||
|
||||
const driverSite = computed(() => {
|
||||
switch (exchangeRateStore.currentExchangeRate.driver) {
|
||||
case 'currency_converter':
|
||||
return `https://www.currencyconverterapi.com`
|
||||
|
||||
case 'currency_freak':
|
||||
return 'https://currencyfreaks.com'
|
||||
|
||||
case 'currency_layer':
|
||||
return 'https://currencylayer.com'
|
||||
|
||||
case 'open_exchange_rate':
|
||||
return 'https://openexchangerates.org'
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => exchangeRateStore)
|
||||
)
|
||||
|
||||
function dismiss() {
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
function removeUsedSelectedCurrencies() {
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
currenciesAlredayInUsed.value.forEach((uc) => {
|
||||
currencies.forEach((c, i) => {
|
||||
if (c === uc) {
|
||||
currencies.splice(i, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
function resetCurrency() {
|
||||
exchangeRateStore.currentExchangeRate.key = null
|
||||
exchangeRateStore.currentExchangeRate.currencies = []
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
}
|
||||
|
||||
function resetModalData() {
|
||||
exchangeRateStore.supportedCurrencies = []
|
||||
currenctPorivderOldCurrencies.value = []
|
||||
exchangeRateStore.currentExchangeRate = {
|
||||
id: null,
|
||||
name: '',
|
||||
driver: '',
|
||||
key: '',
|
||||
active: true,
|
||||
currencies: [],
|
||||
}
|
||||
|
||||
exchangeRateStore.currencyConverter = {
|
||||
type: '',
|
||||
url: '',
|
||||
}
|
||||
currenciesAlredayInUsed.value = []
|
||||
}
|
||||
|
||||
async function fetchInitialData() {
|
||||
exchangeRateStore.currentExchangeRate.driver = 'currency_converter'
|
||||
let params = {}
|
||||
if (exchangeRateStore.isEdit) {
|
||||
params.provider_id = exchangeRateStore.currentExchangeRate.id
|
||||
}
|
||||
isFetchingInitialData.value = true
|
||||
await exchangeRateStore.fetchDefaultProviders()
|
||||
await exchangeRateStore.fetchActiveCurrency(params)
|
||||
|
||||
currenctPorivderOldCurrencies.value =
|
||||
exchangeRateStore.currentExchangeRate.currencies
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCurrencyConverter.value,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchServers()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore.currentExchangeRate.key,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => exchangeRateStore?.currencyConverter?.type,
|
||||
(newVal, oldValue) => {
|
||||
if (newVal) {
|
||||
fetchCurrencies()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fetchCurrencies = debounce(fetchCurrencies, 500)
|
||||
|
||||
function validate() {
|
||||
v$.value.$touch()
|
||||
checkingIsActiveCurrencies()
|
||||
if (
|
||||
v$.value.$invalid ||
|
||||
(currenciesAlredayInUsed.value.length &&
|
||||
exchangeRateStore.currentExchangeRate.active)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function submitExchangeRate() {
|
||||
if (validate()) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
...exchangeRateStore.currentExchangeRate,
|
||||
}
|
||||
if (isCurrencyConverter.value) {
|
||||
data.driver_config = {
|
||||
...exchangeRateStore.currencyConverter,
|
||||
}
|
||||
if (!isDedicatedServer.value) {
|
||||
data.driver_config.url = ''
|
||||
}
|
||||
}
|
||||
const action = exchangeRateStore.isEdit
|
||||
? exchangeRateStore.updateProvider
|
||||
: exchangeRateStore.addProvider
|
||||
isSaving.value = true
|
||||
|
||||
await action(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeExchangeRateModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchServers() {
|
||||
let res = await exchangeRateStore.getCurrencyConverterServers()
|
||||
serverOptions.value = res.data.currency_converter_servers
|
||||
exchangeRateStore.currencyConverter.type = 'FREE'
|
||||
}
|
||||
|
||||
function fetchCurrencies() {
|
||||
const { driver, key } = exchangeRateStore.currentExchangeRate
|
||||
if (driver && key) {
|
||||
isFetchingCurrencies.value = true
|
||||
let data = {
|
||||
driver: driver,
|
||||
key: key,
|
||||
}
|
||||
if (
|
||||
isCurrencyConverter.value &&
|
||||
!exchangeRateStore.currencyConverter.type
|
||||
) {
|
||||
isFetchingCurrencies.value = false
|
||||
return
|
||||
}
|
||||
if (exchangeRateStore?.currencyConverter?.type) {
|
||||
data.type = exchangeRateStore.currencyConverter.type
|
||||
}
|
||||
|
||||
exchangeRateStore
|
||||
.fetchCurrencies(data)
|
||||
.then((res) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetchingCurrencies.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function checkingIsActiveCurrencies(showError = true) {
|
||||
currenciesAlredayInUsed.value = []
|
||||
const { currencies } = exchangeRateStore.currentExchangeRate
|
||||
|
||||
if (currencies.length && exchangeRateStore.activeUsedCurrencies?.length) {
|
||||
currencies.forEach((curr) => {
|
||||
if (exchangeRateStore.activeUsedCurrencies.includes(curr)) {
|
||||
currenciesAlredayInUsed.value.push(curr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeExchangeRateModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
resetModalData()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeDiskModal" @open="loadData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeDiskModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-disk-modal">
|
||||
<component
|
||||
:is="diskStore.selected_driver"
|
||||
:loading="isLoading"
|
||||
:disks="diskStore.getDiskDrivers"
|
||||
:is-edit="isEdit"
|
||||
@onChangeDisk="(val) => diskChange(val)"
|
||||
@submit="createNewDisk"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeDiskModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isRequestFire(slotProps)"
|
||||
:disabled="isRequestFire(slotProps)"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isRequestFire(slotProps)"
|
||||
name="SaveIcon"
|
||||
class="w-6 mr-2"
|
||||
/>
|
||||
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import Dropbox from '@/scripts/admin/components/modal-components/disks/DropboxDisk.vue'
|
||||
import Local from '@/scripts/admin/components/modal-components/disks/LocalDisk.vue'
|
||||
import S3 from '@/scripts/admin/components/modal-components/disks/S3Disk.vue'
|
||||
import DoSpaces from '@/scripts/admin/components/modal-components/disks/DoSpacesDisk.vue'
|
||||
export default {
|
||||
components: {
|
||||
Dropbox,
|
||||
Local,
|
||||
S3,
|
||||
DoSpaces,
|
||||
},
|
||||
setup() {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let isEdit = ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
if (modalStore.id) {
|
||||
isEdit.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'FileDiskModal'
|
||||
})
|
||||
|
||||
function isRequestFire(slotProps) {
|
||||
return (
|
||||
slotProps && (slotProps.diskData.isLoading.value || isLoading.value)
|
||||
)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let res = await diskStore.fetchDiskDrivers()
|
||||
if (isEdit.value) {
|
||||
diskStore.selected_driver = modalStore.data.driver
|
||||
} else {
|
||||
diskStore.selected_driver = res.data.drivers[0].value
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
async function createNewDisk(data) {
|
||||
Object.assign(diskStore.diskConfigData, data)
|
||||
isLoading.value = true
|
||||
|
||||
let formData = {
|
||||
id: modalStore.id,
|
||||
...data,
|
||||
}
|
||||
|
||||
let response = null
|
||||
const action = isEdit.value ? diskStore.updateDisk : diskStore.createDisk
|
||||
response = await action(formData)
|
||||
isLoading.value = false
|
||||
modalStore.refreshData()
|
||||
closeDiskModal()
|
||||
}
|
||||
|
||||
function closeDiskModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
|
||||
function diskChange(value) {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.diskConfigData.selected_driver = value
|
||||
}
|
||||
|
||||
return {
|
||||
isEdit,
|
||||
createNewDisk,
|
||||
isRequestFire,
|
||||
diskStore,
|
||||
closeDiskModal,
|
||||
loadData,
|
||||
diskChange,
|
||||
modalStore,
|
||||
isLoading,
|
||||
modalActive,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeItemModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="item-modal">
|
||||
<form action="" @submit.prevent="submitItemData">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('items.name')"
|
||||
required
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItem.name"
|
||||
type="text"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.price')">
|
||||
<BaseMoney
|
||||
:key="companyStore.selectedCompanyCurrency"
|
||||
v-model="price"
|
||||
:currency="companyStore.selectedCompanyCurrency"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup :label="$t('items.unit')">
|
||||
<BaseMultiselect
|
||||
v-model="itemStore.currentItem.unit_id"
|
||||
label="name"
|
||||
:options="itemStore.itemUnits"
|
||||
value-prop="id"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
:placeholder="$t('items.select_a_unit')"
|
||||
searchable
|
||||
track-by="id"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isTaxPerItemEnabled"
|
||||
:label="$t('items.taxes')"
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="taxes"
|
||||
:options="getTaxTypes"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
class="w-full"
|
||||
:can-deselect="false"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="id"
|
||||
object
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('items.description')"
|
||||
:error="
|
||||
v$.description.$error && v$.description.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="itemStore.currentItem.description"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.description.$error"
|
||||
@input="v$.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeItemModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ itemStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
minValue,
|
||||
helpers,
|
||||
alpha,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
|
||||
const emit = defineEmits(['newItem'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const itemStore = useItemStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const taxPerItemSetting = ref(companyStore.selectedCompanySettings.tax_per_item)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'ItemModal'
|
||||
)
|
||||
|
||||
const price = computed({
|
||||
get: () => itemStore.currentItem.price / 100,
|
||||
set: (value) => {
|
||||
itemStore.currentItem.price = Math.round(value * 100)
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () =>
|
||||
itemStore.currentItem.taxes.map((tax) => {
|
||||
if (tax) {
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: tax.name + ' (' + tax.percent + '%)',
|
||||
}
|
||||
}
|
||||
}),
|
||||
set: (value) => {
|
||||
itemStore.$patch((state) => {
|
||||
state.currentItem.taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const isTaxPerItemEnabled = computed(() => {
|
||||
return taxPerItemSetting.value === 'YES'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItem)
|
||||
)
|
||||
|
||||
const getTaxTypes = computed(() => {
|
||||
return taxTypeStore.taxTypes.map((tax) => {
|
||||
return { ...tax, tax_name: tax.name + ' (' + tax.percent + '%)' }
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
v$.value.$reset()
|
||||
itemStore.fetchItemUnits({ limit: 'all' })
|
||||
})
|
||||
|
||||
async function submitItemData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
...itemStore.currentItem,
|
||||
taxes: itemStore.currentItem.taxes.map((tax) => {
|
||||
return {
|
||||
tax_type_id: tax.id,
|
||||
amount: (price.value * tax.percent) / 100,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
collective_tax: 0,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const action = itemStore.isEdit ? itemStore.updateItem : itemStore.addItem
|
||||
|
||||
await action(data).then((res) => {
|
||||
isLoading.value = false
|
||||
if (res.data.data) {
|
||||
if (modalStore.data) {
|
||||
modalStore.refreshData(res.data.data)
|
||||
}
|
||||
}
|
||||
closeItemModal()
|
||||
})
|
||||
}
|
||||
|
||||
function closeItemModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
itemStore.resetCurrentItem()
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'ItemUnitModal'"
|
||||
@close="closeItemUnitModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeItemUnitModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitItemUnit">
|
||||
<div class="p-8 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.items.unit_name')"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="itemStore.currentItemUnit.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-gray-200 border-solid border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
class="mr-3 text-sm"
|
||||
@click="closeItemUnitModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
itemStore.isItemUnitEdit ? $t('general.update') : $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const itemStore = useItemStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => itemStore.currentItemUnit)
|
||||
)
|
||||
|
||||
async function submitItemUnit() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = itemStore.isItemUnitEdit
|
||||
? itemStore.updateItemUnit
|
||||
: itemStore.addItemUnit
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
await action(itemStore.currentItemUnit)
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeItemUnitModal()
|
||||
isSaving.value = false
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closeItemUnitModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
itemStore.currentItemUnit = {
|
||||
id: null,
|
||||
name: '',
|
||||
}
|
||||
|
||||
modalStore.$reset()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeTestModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTestModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="onTestMailSend">
|
||||
<div class="p-4 md:p-8">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
:error="v$.formData.to.$error && v$.formData.to.$errors[0].$message"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
ref="to"
|
||||
v-model="formData.to"
|
||||
type="text"
|
||||
:invalid="v$.formData.to.$error"
|
||||
@input="v$.formData.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
:error="
|
||||
v$.formData.subject.$error &&
|
||||
v$.formData.subject.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.subject"
|
||||
type="text"
|
||||
:invalid="v$.formData.subject.$error"
|
||||
@input="v$.formData.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.message')"
|
||||
:error="
|
||||
v$.formData.message.$error &&
|
||||
v$.formData.message.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="formData.message"
|
||||
rows="4"
|
||||
cols="50"
|
||||
:invalid="v$.formData.message.$error"
|
||||
@input="v$.formData.message.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="closeTestModal()"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isSaving" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="PaperAirplaneIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, maxLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
let isSaving = ref(false)
|
||||
let formData = reactive({
|
||||
to: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'MailTestModal'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
formData: {
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.subject_maxlength'),
|
||||
maxLength(100)
|
||||
),
|
||||
},
|
||||
message: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.message_maxlength'),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, { formData })
|
||||
|
||||
function resetFormData() {
|
||||
formData.id = ''
|
||||
formData.to = ''
|
||||
formData.subject = ''
|
||||
formData.message = ''
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
async function onTestMailSend() {
|
||||
v$.value.formData.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
let response = await mailDriverStore.sendTestMail(formData)
|
||||
if (response.data) {
|
||||
closeTestModal()
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
function closeTestModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
modalStore.resetModalData()
|
||||
resetFormData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeNoteModal" @open="setFields">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeNoteModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitNote">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.name')"
|
||||
variant="vertical"
|
||||
:error="
|
||||
v$.currentNote.name.$error &&
|
||||
v$.currentNote.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="noteStore.currentNote.name"
|
||||
:invalid="v$.currentNote.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentNote.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.type')"
|
||||
:error="
|
||||
v$.currentNote.type.$error &&
|
||||
v$.currentNote.type.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="noteStore.currentNote.type"
|
||||
:options="types"
|
||||
value-prop="type"
|
||||
class="mt-2"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.notes.notes')"
|
||||
:error="
|
||||
v$.currentNote.notes.$error &&
|
||||
v$.currentNote.notes.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="noteStore.currentNote.notes"
|
||||
:invalid="v$.currentNote.notes.$error"
|
||||
:fields="fields"
|
||||
@input="v$.currentNote.notes.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
px-4
|
||||
py-4
|
||||
border-t border-solid border-gray-light
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeNoteModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ noteStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useNotesStore } from '@/scripts/admin/stores/note'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const noteStore = useNotesStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
const types = reactive(['Invoice', 'Estimate', 'Payment'])
|
||||
let fields = ref(['customer', 'customerCustom'])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'NoteModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentNote: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
notes: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => noteStore)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => noteStore.currentNote.type,
|
||||
(val) => {
|
||||
setFields()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (route.name === 'estimates.create') {
|
||||
noteStore.currentNote.type = 'Estimate'
|
||||
} else if (route.name === 'invoices.create') {
|
||||
noteStore.currentNote.type = 'Invoice'
|
||||
} else {
|
||||
noteStore.currentNote.type = 'Payment'
|
||||
}
|
||||
})
|
||||
|
||||
function setFields() {
|
||||
fields.value = ['customer', 'customerCustom']
|
||||
|
||||
if (noteStore.currentNote.type == 'Invoice') {
|
||||
fields.value.push('invoice', 'invoiceCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Estimate') {
|
||||
fields.value.push('estimate', 'estimateCustom')
|
||||
}
|
||||
|
||||
if (noteStore.currentNote.type == 'Payment') {
|
||||
fields.value.push('payment', 'paymentCustom')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNote() {
|
||||
v$.value.currentNote.$touch()
|
||||
if (v$.value.currentNote.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
if (noteStore.isEdit) {
|
||||
let data = {
|
||||
id: noteStore.currentNote.id,
|
||||
...noteStore.currentNote,
|
||||
}
|
||||
await noteStore
|
||||
.updateNote(data)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_updated'),
|
||||
})
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
} else {
|
||||
await noteStore
|
||||
.addNote(noteStore.currentNote)
|
||||
.then((res) => {
|
||||
isSaving.value = false
|
||||
if (res.data) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: t('settings.customization.notes.note_added'),
|
||||
})
|
||||
|
||||
if (
|
||||
(route.name === 'invoices.create' &&
|
||||
res.data.data.type === 'Invoice') ||
|
||||
(route.name === 'invoices.edit' && res.data.data.type === 'Invoice')
|
||||
) {
|
||||
invoiceStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'estimates.create' &&
|
||||
res.data.data.type === 'Estimate') ||
|
||||
(route.name === 'estimates.edit' &&
|
||||
res.data.data.type === 'Estimate')
|
||||
) {
|
||||
estimateStore.selectNote(res.data.data)
|
||||
}
|
||||
|
||||
if (
|
||||
(route.name === 'payments.create' &&
|
||||
res.data.data.type === 'Payment') ||
|
||||
(route.name === 'payments.edit' && res.data.data.type === 'Payment')
|
||||
) {
|
||||
paymentStore.selectNote(res.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeNoteModal()
|
||||
})
|
||||
.catch((err) => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeNoteModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
noteStore.resetCurrentNote()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.note-modal {
|
||||
.header-editior .editor-menu-bar {
|
||||
margin-left: 0.5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closePaymentModeModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closePaymentModeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitPaymentMode">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.payment_modes.mode_name')"
|
||||
:error="
|
||||
v$.currentPaymentMode.name.$error &&
|
||||
v$.currentPaymentMode.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentStore.currentPaymentMode.name"
|
||||
:invalid="v$.currentPaymentMode.name.$error"
|
||||
@input="v$.currentPaymentMode.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
variant="primary-outline"
|
||||
class="mr-3"
|
||||
type="button"
|
||||
@click="closePaymentModeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{
|
||||
paymentStore.currentPaymentMode.id
|
||||
? $t('general.update')
|
||||
: $t('general.save')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentPaymentMode: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => paymentStore)
|
||||
)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'PaymentModeModal'
|
||||
})
|
||||
|
||||
async function submitPaymentMode() {
|
||||
v$.value.currentPaymentMode.$touch()
|
||||
|
||||
if (v$.value.currentPaymentMode.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = paymentStore.currentPaymentMode.id
|
||||
? paymentStore.updatePaymentMode
|
||||
: paymentStore.addPaymentMode
|
||||
isSaving.value = true
|
||||
await action(paymentStore.currentPaymentMode)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closePaymentModeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function closePaymentModeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
paymentStore.currentPaymentMode = {
|
||||
id: '',
|
||||
name: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeRolesModal">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeRolesModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="submitRoleData">
|
||||
<div class="px-4 md:px-8 py-4 md:py-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.roles.name')"
|
||||
class="mt-3"
|
||||
:error="v$.name.$error && v$.name.$errors[0].$message"
|
||||
required
|
||||
:content-loading="isFetchingInitialData"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="roleStore.currentRole.name"
|
||||
:invalid="v$.name.$error"
|
||||
type="text"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<h6
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-gray-800
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
{{ $tc('settings.roles.permission', 2) }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</h6>
|
||||
<div
|
||||
class="
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
text-gray-300
|
||||
px-4
|
||||
md:px-8
|
||||
py-1.5
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(true)"
|
||||
>
|
||||
{{ $t('settings.roles.select_all') }}
|
||||
</a>
|
||||
/
|
||||
<a
|
||||
class="cursor-pointer text-primary-400"
|
||||
@click="setSelectAll(false)"
|
||||
>
|
||||
{{ $t('settings.roles.none') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 py-3">
|
||||
<div
|
||||
class="
|
||||
grid grid-cols-1
|
||||
sm:grid-cols-2
|
||||
md:grid-cols-3
|
||||
lg:grid-cols-4
|
||||
gap-4
|
||||
px-8
|
||||
sm:px-8
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(abilityGroup, gIndex) in roleStore.abilitiesList"
|
||||
:key="gIndex"
|
||||
class="flex flex-col space-y-1"
|
||||
>
|
||||
<p class="text-sm text-gray-500 border-b border-gray-200 pb-1 mb-2">
|
||||
{{ gIndex }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(ability, index) in abilityGroup"
|
||||
:key="index"
|
||||
class="flex"
|
||||
>
|
||||
<BaseCheckbox
|
||||
v-model="roleStore.currentRole.abilities"
|
||||
:set-initial-value="true"
|
||||
variant="primary"
|
||||
:disabled="ability.disabled"
|
||||
:label="ability.name"
|
||||
:value="ability"
|
||||
@update:modelValue="onUpdateAbility(ability)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="v$.abilities.$error"
|
||||
class="block mt-0.5 text-sm text-red-500"
|
||||
>
|
||||
{{ v$.abilities.$errors[0].$message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeRolesModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ !roleStore.isEdit ? $t('general.save') : $t('general.update') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useRoleStore } from '@/scripts/admin/stores/role'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const roleStore = useRoleStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
let isFetchingInitialData = ref(false)
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'RolesModal'
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
abilities: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.at_least_one_ability'),
|
||||
required
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => roleStore.currentRole)
|
||||
)
|
||||
|
||||
async function submitRoleData() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = roleStore.isEdit ? roleStore.updateRole : roleStore.addRole
|
||||
isSaving.value = true
|
||||
await action(roleStore.currentRole)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
closeRolesModal()
|
||||
} catch (error) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateAbility(currentAbility) {
|
||||
const fd = roleStore.currentRole.abilities.find(
|
||||
(_abl) => _abl.ability === currentAbility.ability
|
||||
)
|
||||
|
||||
if (!fd && currentAbility?.depends_on?.length) {
|
||||
enableAbilities(currentAbility)
|
||||
return
|
||||
}
|
||||
|
||||
currentAbility?.depends_on?.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (_d === _a.ability) {
|
||||
_a.disabled = true
|
||||
|
||||
let found = roleStore.currentRole.abilities.find(
|
||||
(_af) => _af.ability === _d
|
||||
)
|
||||
|
||||
if (!found) {
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setSelectAll(checked) {
|
||||
let dependList = []
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a?.depends_on && (dependList = [...dependList, ..._a.depends_on])
|
||||
})
|
||||
})
|
||||
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
if (dependList.includes(_a.ability)) {
|
||||
checked ? (_a.disabled = true) : (_a.disabled = false)
|
||||
}
|
||||
roleStore.currentRole.abilities.push(_a)
|
||||
})
|
||||
})
|
||||
|
||||
if (!checked) roleStore.currentRole.abilities = []
|
||||
}
|
||||
|
||||
function enableAbilities(ability) {
|
||||
ability.depends_on.forEach((_d) => {
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
// CHECK IF EXISTS IN CURRENT ROLE ABILITIES
|
||||
let found = roleStore.currentRole.abilities.find((_r) =>
|
||||
_r.depends_on?.includes(_a.ability)
|
||||
)
|
||||
|
||||
if (_d === _a.ability && !found) {
|
||||
_a.disabled = false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function closeRolesModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
roleStore.currentRole = {
|
||||
id: null,
|
||||
name: '',
|
||||
abilities: [],
|
||||
}
|
||||
|
||||
// Enable all disabled ability
|
||||
Object.keys(roleStore.abilitiesList).forEach((group) => {
|
||||
roleStore.abilitiesList[group].forEach((_a) => {
|
||||
_a.disabled = false
|
||||
})
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<div
|
||||
v-if="modalStore.data"
|
||||
class="grid grid-cols-3 gap-2 p-1 overflow-x-auto"
|
||||
>
|
||||
<div
|
||||
v-for="(template, index) in modalStore.data.templates"
|
||||
:key="index"
|
||||
:class="{
|
||||
'border border-solid border-primary-500':
|
||||
selectedTemplate === template.name,
|
||||
}"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
m-2
|
||||
border border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:border-primary-300
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="template.path"
|
||||
:alt="template.name"
|
||||
class="w-full"
|
||||
@click="selectedTemplate = template.name"
|
||||
/>
|
||||
<img
|
||||
v-if="selectedTemplate === template.name"
|
||||
:alt="template.name"
|
||||
class="absolute z-10 w-5 h-5 text-primary-500"
|
||||
style="top: -6px; right: -5px"
|
||||
:src="getTickImage()"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
'w-full p-1 bg-gray-200 text-sm text-center absolute bottom-0 left-0',
|
||||
{
|
||||
'text-primary-500 bg-primary-100':
|
||||
selectedTemplate === template.name,
|
||||
'text-gray-600': selectedTemplate != template.name,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ template.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid">
|
||||
<BaseButton class="mr-3" variant="primary-outline" @click="closeModal">
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" @click="chooseTemplate()">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon name="SaveIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ $t('general.choose') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const selectedTemplate = ref('')
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SelectTemplate'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
function setData() {
|
||||
if (modalStore.data.store[modalStore.data.storeProp].template_name) {
|
||||
selectedTemplate.value =
|
||||
modalStore.data.store[modalStore.data.storeProp].template_name
|
||||
} else {
|
||||
selectedTemplate.value = modalStore.data.templates[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseTemplate() {
|
||||
await modalStore.data.store.setTemplate(selectedTemplate.value)
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function getTickImage() {
|
||||
const imgUrl = new URL('/img/tick.png', import.meta.url)
|
||||
return imgUrl
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
modalStore.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendEstimateModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendEstimateModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="estimateMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup :label="$t('general.body')" required>
|
||||
<BaseCustomInput
|
||||
v-model="estimateMailForm.body"
|
||||
:fields="estimateMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PhotographIcon" class="h-5 mr-2" />
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendEstimateModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm"
|
||||
>
|
||||
<BaseIcon v-if="!isLoading" name="PaperAirplaneIcon" class="mr-2" />
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watchEffect, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const estimateMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'estimate',
|
||||
'estimateCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
let estimateMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Estimate',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendEstimateModal'
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => estimateMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
estimateMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
estimateMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
estimateMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
estimateMailForm.body =
|
||||
companyStore.selectedCompanySettings.estimate_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await estimateStore.previewEstimate(
|
||||
estimateMailForm
|
||||
)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await estimateStore.sendEstimate(estimateMailForm)
|
||||
|
||||
isLoading.value = false
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendEstimateModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('estimates.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendEstimateModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendInvoiceModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendInvoiceModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="invoiceMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="invoiceMailForm.body"
|
||||
:fields="invoiceMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendInvoiceModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="submitForm()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const invoiceMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'invoice',
|
||||
'invoiceCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const invoiceMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Invoice',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendInvoiceModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => invoiceMailForm)
|
||||
)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
|
||||
invoiceMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
invoiceMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
invoiceMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
invoiceMailForm.body = companyStore.selectedCompanySettings.invoice_mail_body
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await invoiceStore.previewInvoice(invoiceMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await invoiceStore.sendInvoice(invoiceMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendInvoiceModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('invoices.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendInvoiceModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
@close="closeSendPaymentModal"
|
||||
@open="setInitialData"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalTitle }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeSendPaymentModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="!isPreview" action="">
|
||||
<div class="px-8 py-8 sm:p-6">
|
||||
<BaseInputGrid layout="one-column" class="col-span-7">
|
||||
<BaseInputGroup
|
||||
:label="$t('general.from')"
|
||||
required
|
||||
:error="v$.from.$error && v$.from.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.from"
|
||||
type="text"
|
||||
:invalid="v$.from.$error"
|
||||
@input="v$.from.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.to')"
|
||||
required
|
||||
:error="v$.to.$error && v$.to.$errors[0].$message"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.to"
|
||||
type="text"
|
||||
:invalid="v$.to.$error"
|
||||
@input="v$.to.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:error="v$.subject.$error && v$.subject.$errors[0].$message"
|
||||
:label="$t('general.subject')"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="paymentMailForm.subject"
|
||||
type="text"
|
||||
:invalid="v$.subject.$error"
|
||||
@input="v$.subject.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
<BaseInputGroup
|
||||
:label="$t('general.body')"
|
||||
:error="v$.body.$error && v$.body.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="paymentMailForm.body"
|
||||
:fields="paymentMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
class="mr-3"
|
||||
@click="sendPaymentData"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
:class="slotProps.class"
|
||||
name="PhotographIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.preview') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<div class="my-6 mx-4 border border-gray-200 relative">
|
||||
<BaseButton
|
||||
class="absolute top-4 right-4"
|
||||
:disabled="isLoading"
|
||||
variant="primary-outline"
|
||||
@click="cancelPreview"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="h-5 mr-2" />
|
||||
Edit
|
||||
</BaseButton>
|
||||
|
||||
<iframe
|
||||
:src="templateUrl"
|
||||
frameborder="0"
|
||||
class="w-full"
|
||||
style="min-height: 500px"
|
||||
></iframe>
|
||||
</div>
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeSendPaymentModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
variant="primary"
|
||||
type="button"
|
||||
@click="sendPaymentData()"
|
||||
>
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="PaperAirplaneIcon"
|
||||
class="h-5 mr-2"
|
||||
/>
|
||||
{{ $t('general.send') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { ref, reactive, computed, watch, watchEffect } from 'vue'
|
||||
import { usePaymentStore } from '@/scripts/admin/stores/payment'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useMailDriverStore } from '@/scripts/admin/stores/mail-driver'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
|
||||
const paymentStore = usePaymentStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const mailDriversStore = useMailDriverStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
let isLoading = ref(false)
|
||||
const templateUrl = ref('')
|
||||
const isPreview = ref(false)
|
||||
|
||||
const paymentMailFields = ref([
|
||||
'customer',
|
||||
'customerCustom',
|
||||
'payments',
|
||||
'paymentsCustom',
|
||||
'company',
|
||||
])
|
||||
|
||||
const paymentMailForm = reactive({
|
||||
id: null,
|
||||
from: null,
|
||||
to: null,
|
||||
subject: 'New Payment',
|
||||
body: null,
|
||||
})
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'SendPaymentModal'
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return modalStore.title
|
||||
})
|
||||
|
||||
const modalData = computed(() => {
|
||||
return modalStore.data
|
||||
})
|
||||
|
||||
const rules = {
|
||||
from: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
to: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
email: helpers.withMessage(t('validation.email_incorrect'), email),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
body: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(rules, paymentMailForm)
|
||||
|
||||
function cancelPreview() {
|
||||
isPreview.value = false
|
||||
}
|
||||
|
||||
async function setInitialData() {
|
||||
let admin = await companyStore.fetchBasicMailConfig()
|
||||
paymentMailForm.id = modalStore.id
|
||||
|
||||
if (admin.data) {
|
||||
paymentMailForm.from = admin.data.from_mail
|
||||
}
|
||||
|
||||
if (modalData.value) {
|
||||
paymentMailForm.to = modalData.value.customer.email
|
||||
}
|
||||
|
||||
paymentMailForm.body = companyStore.selectedCompanySettings.payment_mail_body
|
||||
}
|
||||
|
||||
async function sendPaymentData() {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
if (!isPreview.value) {
|
||||
const previewResponse = await paymentStore.previewPayment(paymentMailForm)
|
||||
isLoading.value = false
|
||||
|
||||
isPreview.value = true
|
||||
var blob = new Blob([previewResponse.data], { type: 'text/html' })
|
||||
templateUrl.value = URL.createObjectURL(blob)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const response = await paymentStore.sendEmail(paymentMailForm)
|
||||
|
||||
if (response.data.success) {
|
||||
closeSendPaymentModal()
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t('payments.something_went_wrong'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function closeSendPaymentModal() {
|
||||
setTimeout(() => {
|
||||
v$.value.$reset()
|
||||
isPreview.value = false
|
||||
templateUrl.value = null
|
||||
modalStore.resetModalData()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalStore.active && modalStore.componentName === 'TaxTypeModal'"
|
||||
@close="closeTaxTypeModal"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeTaxTypeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form action="" @submit.prevent="submitTaxTypeData">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.name')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.name.$error &&
|
||||
v$.currentTaxType.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="taxTypeStore.currentTaxType.name"
|
||||
:invalid="v$.currentTaxType.name.$error"
|
||||
type="text"
|
||||
@input="v$.currentTaxType.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.percent')"
|
||||
variant="horizontal"
|
||||
:error="
|
||||
v$.currentTaxType.percent.$error &&
|
||||
v$.currentTaxType.percent.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMoney
|
||||
v-model="taxTypeStore.currentTaxType.percent"
|
||||
:currency="{
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
symbol: '% ',
|
||||
precision: 2,
|
||||
masked: false,
|
||||
}"
|
||||
:invalid="v$.currentTaxType.percent.$error"
|
||||
class="
|
||||
relative
|
||||
w-full
|
||||
focus:border focus:border-solid focus:border-primary
|
||||
"
|
||||
@input="v$.currentTaxType.percent.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.description')"
|
||||
:error="
|
||||
v$.currentTaxType.description.$error &&
|
||||
v$.currentTaxType.description.$errors[0].$message
|
||||
"
|
||||
variant="horizontal"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="taxTypeStore.currentTaxType.description"
|
||||
:invalid="v$.currentTaxType.description.$error"
|
||||
rows="4"
|
||||
cols="50"
|
||||
@input="v$.currentTaxType.description.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('tax_types.compound_tax')"
|
||||
variant="horizontal"
|
||||
class="flex flex-row-reverse"
|
||||
>
|
||||
<BaseSwitch
|
||||
v-model="taxTypeStore.currentTaxType.compound_tax"
|
||||
class="flex items-center"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border--200 border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeTaxTypeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ taxTypeStore.isEdit ? $t('general.update') : $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/admin/stub/abilities'
|
||||
import {
|
||||
required,
|
||||
minLength,
|
||||
maxLength,
|
||||
between,
|
||||
helpers,
|
||||
} from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const { t, tm } = useI18n()
|
||||
let isSaving = ref(false)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentTaxType: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
percent: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
between: helpers.withMessage(
|
||||
t('validation.enter_valid_tax_rate'),
|
||||
between(0, 100)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.description_maxlength', { count: 255 }),
|
||||
maxLength(255)
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => taxTypeStore)
|
||||
)
|
||||
|
||||
async function submitTaxTypeData() {
|
||||
v$.value.currentTaxType.$touch()
|
||||
if (v$.value.currentTaxType.$invalid) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const action = taxTypeStore.isEdit
|
||||
? taxTypeStore.updateTaxType
|
||||
: taxTypeStore.addTaxType
|
||||
isSaving.value = true
|
||||
let res = await action(taxTypeStore.currentTaxType)
|
||||
isSaving.value = false
|
||||
modalStore.refreshData ? modalStore.refreshData(res.data.data) : ''
|
||||
closeTaxTypeModal()
|
||||
} catch (err) {
|
||||
isSaving.value = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function SelectTax(taxData) {
|
||||
let amount = 0
|
||||
if (taxData.compound_tax && estimateStore.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((estimateStore.getSubtotalWithDiscount +
|
||||
estimateStore.getTotalSimpleTax) *
|
||||
taxData.percent) /
|
||||
100
|
||||
)
|
||||
} else if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
|
||||
amount = Math.round(
|
||||
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
|
||||
)
|
||||
}
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
amount,
|
||||
}
|
||||
estimateStore.$patch((state) => {
|
||||
state.newEstimate.taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function selectItemTax(taxData) {
|
||||
if (modalStore.data) {
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: taxData.name,
|
||||
percent: taxData.percent,
|
||||
compound_tax: taxData.compound_tax,
|
||||
tax_type_id: taxData.id,
|
||||
}
|
||||
modalStore.refreshData(data)
|
||||
}
|
||||
}
|
||||
|
||||
function closeTaxTypeModal() {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
taxTypeStore.resetCurrentTaxType()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setAddress">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex flex-col">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ modalStore.content }}
|
||||
</p>
|
||||
</div>
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="h-6 w-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="saveCustomerAddress">
|
||||
<div class="p-4 sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.state.$error && v$.state.$errors[0].$message"
|
||||
:label="$t('customers.state')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.state"
|
||||
type="text"
|
||||
name="shippingState"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.state.$error"
|
||||
@input="v$.state.$touch()"
|
||||
:placeholder="$t('settings.taxations.state_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.city.$error && v$.city.$errors[0].$message"
|
||||
:label="$t('customers.city')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.city"
|
||||
type="text"
|
||||
name="shippingCity"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.city.$error"
|
||||
@input="v$.city.$touch()"
|
||||
:placeholder="$t('settings.taxations.city_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="
|
||||
v$.address_street_1.$error &&
|
||||
v$.address_street_1.$errors[0].$message
|
||||
"
|
||||
:label="$t('customers.address')"
|
||||
>
|
||||
<BaseTextarea
|
||||
v-model="address.address_street_1"
|
||||
rows="2"
|
||||
cols="50"
|
||||
class="mt-1 md:mt-0"
|
||||
:invalid="v$.address_street_1.$error"
|
||||
@input="v$.address_street_1.$touch()"
|
||||
:placeholder="$t('settings.taxations.address_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
required
|
||||
:error="v$.zip.$error && v$.zip.$errors[0].$message"
|
||||
:label="$t('customers.zip_code')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="address.zip"
|
||||
:invalid="v$.zip.$error"
|
||||
@input="v$.zip.$touch()"
|
||||
type="text"
|
||||
class="mt-1 md:mt-0"
|
||||
:placeholder="$t('settings.taxations.zip_placeholder')"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton :loading="isLoading" variant="primary" type="submit">
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isLoading"
|
||||
name="SaveIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { helpers, required } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const address = reactive({
|
||||
state: '',
|
||||
city: '',
|
||||
address_street_1: '',
|
||||
zip: '',
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'TaxationAddressModal'
|
||||
)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
state: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
city: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
address_street_1: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
zip: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => address)
|
||||
)
|
||||
|
||||
async function saveCustomerAddress() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
address,
|
||||
}
|
||||
if (modalStore.id) {
|
||||
data.customer_id = modalStore.id
|
||||
}
|
||||
// replace '/n' with empty string
|
||||
address.address_street_1 = address.address_street_1.replace(
|
||||
/(\r\n|\n|\r)/gm,
|
||||
''
|
||||
)
|
||||
|
||||
isLoading.value = true
|
||||
await taxTypeStore
|
||||
.fetchSalesTax(data)
|
||||
.then((res) => {
|
||||
isLoading.value = false
|
||||
emit('addTax', res.data.data)
|
||||
closeModal()
|
||||
})
|
||||
.catch((e) => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
const emit = defineEmits(['addTax'])
|
||||
|
||||
function setAddress() {
|
||||
address.state = modalStore?.data?.state
|
||||
address.city = modalStore?.data?.city
|
||||
address.address_street_1 = modalStore?.data?.address_street_1
|
||||
address.zip = modalStore?.data?.zip
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @open="setData">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeCustomFieldModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCustomFieldData">
|
||||
<div class="overflow-y-auto max-h-[550px]">
|
||||
<div class="px-4 md:px-8 py-8 overflow-y-auto sm:p-6">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.name')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.name.$error &&
|
||||
v$.currentCustomField.name.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
ref="name"
|
||||
v-model="customFieldStore.currentCustomField.name"
|
||||
:invalid="v$.currentCustomField.name.$error"
|
||||
@input="v$.currentCustomField.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.model')"
|
||||
:error="
|
||||
v$.currentCustomField.model_type.$error &&
|
||||
v$.currentCustomField.model_type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.model_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="customFieldStore.currentCustomField.model_type"
|
||||
:options="modelTypes"
|
||||
:can-deselect="false"
|
||||
:invalid="v$.currentCustomField.model_type.$error"
|
||||
:searchable="true"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
@input="v$.currentCustomField.model_type.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
class="flex items-center space-x-4"
|
||||
:label="$t('settings.custom_fields.required')"
|
||||
>
|
||||
<BaseSwitch v-model="isRequiredField" />
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.type')"
|
||||
:error="
|
||||
v$.currentCustomField.type.$error &&
|
||||
v$.currentCustomField.type.$errors[0].$message
|
||||
"
|
||||
:help-text="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? $t('settings.custom_fields.type_in_use')
|
||||
: ''
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selectedType"
|
||||
:options="dataTypes"
|
||||
:invalid="v$.currentCustomField.type.$error"
|
||||
:disabled="customFieldStore.currentCustomField.in_use"
|
||||
:searchable="true"
|
||||
:can-deselect="false"
|
||||
object
|
||||
@update:modelValue="onSelectedTypeChange"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.label')"
|
||||
required
|
||||
:error="
|
||||
v$.currentCustomField.label.$error &&
|
||||
v$.currentCustomField.label.$errors[0].$message
|
||||
"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.label"
|
||||
:invalid="v$.currentCustomField.label.$error"
|
||||
@input="v$.currentCustomField.label.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="isDropdownSelected"
|
||||
:label="$t('settings.custom_fields.options')"
|
||||
>
|
||||
<OptionCreate @onAdd="addNewOption" />
|
||||
|
||||
<div
|
||||
v-for="(option, index) in customFieldStore.currentCustomField
|
||||
.options"
|
||||
:key="index"
|
||||
class="flex items-center mt-5"
|
||||
>
|
||||
<BaseInput v-model="option.name" class="w-64" />
|
||||
|
||||
<BaseIcon
|
||||
name="MinusCircleIcon"
|
||||
class="ml-1 cursor-pointer"
|
||||
:class="
|
||||
customFieldStore.currentCustomField.in_use
|
||||
? 'text-gray-300'
|
||||
: 'text-red-300'
|
||||
"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.default_value')"
|
||||
class="relative"
|
||||
>
|
||||
<component
|
||||
:is="defaultValueComponent"
|
||||
v-model="customFieldStore.currentCustomField.default_answer"
|
||||
:options="customFieldStore.currentCustomField.options"
|
||||
:default-date-time="
|
||||
customFieldStore.currentCustomField.dateTimeValue
|
||||
"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="!isSwitchTypeSelected"
|
||||
:label="$t('settings.custom_fields.placeholder')"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.placeholder"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.custom_fields.order')"
|
||||
:error="
|
||||
v$.currentCustomField.order.$error &&
|
||||
v$.currentCustomField.order.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="customFieldStore.currentCustomField.order"
|
||||
type="number"
|
||||
:invalid="v$.currentCustomField.order.$error"
|
||||
@input="v$.currentCustomField.order.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
p-4
|
||||
border-t border-solid border-gray-light border-modal-bg
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-3"
|
||||
type="button"
|
||||
variant="primary-outline"
|
||||
@click="closeCustomFieldModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="SaveIcon"
|
||||
/>
|
||||
</template>
|
||||
{{
|
||||
!customFieldStore.isEdit ? $t('general.save') : $t('general.update')
|
||||
}}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed, defineAsyncComponent } from 'vue'
|
||||
import OptionCreate from './OptionsCreate.vue'
|
||||
import moment from 'moment'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, numeric, helpers } from '@vuelidate/validators'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isSaving = ref(false)
|
||||
|
||||
const modelTypes = reactive([
|
||||
'Customer',
|
||||
'Invoice',
|
||||
'Estimate',
|
||||
'Expense',
|
||||
'Payment',
|
||||
])
|
||||
|
||||
const dataTypes = reactive([
|
||||
{ label: 'Text', value: 'Input' },
|
||||
{ label: 'Textarea', value: 'TextArea' },
|
||||
{ label: 'Phone', value: 'Phone' },
|
||||
{ label: 'URL', value: 'Url' },
|
||||
{ label: 'Number', value: 'Number' },
|
||||
{ label: 'Select Field', value: 'Dropdown' },
|
||||
{ label: 'Switch Toggle', value: 'Switch' },
|
||||
{ label: 'Date', value: 'Date' },
|
||||
{ label: 'Time', value: 'Time' },
|
||||
{ label: 'Date & Time', value: 'DateTime' },
|
||||
])
|
||||
|
||||
let selectedType = ref(dataTypes[0])
|
||||
|
||||
const modalActive = computed(() => {
|
||||
return modalStore.active && modalStore.componentName === 'CustomFieldModal'
|
||||
})
|
||||
|
||||
const isSwitchTypeSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Switch Toggle'
|
||||
)
|
||||
|
||||
const isDropdownSelected = computed(
|
||||
() => selectedType.value && selectedType.value.label === 'Select Field'
|
||||
)
|
||||
|
||||
const defaultValueComponent = computed(() => {
|
||||
if (customFieldStore.currentCustomField.type) {
|
||||
return defineAsyncComponent(() =>
|
||||
import(
|
||||
`../../custom-fields/types/${customFieldStore.currentCustomField.type}Type.vue`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const isRequiredField = computed({
|
||||
get: () => customFieldStore.currentCustomField.is_required === 1,
|
||||
set: (value) => {
|
||||
const intVal = value ? 1 : 0
|
||||
customFieldStore.currentCustomField.is_required = intVal
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
currentCustomField: {
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
label: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
model_type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
order: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
type: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => customFieldStore)
|
||||
)
|
||||
|
||||
function setData() {
|
||||
if (customFieldStore.isEdit) {
|
||||
selectedType.value = dataTypes.find(
|
||||
(type) => type.value == customFieldStore.currentCustomField.type
|
||||
)
|
||||
} else {
|
||||
customFieldStore.currentCustomField.model_type = modelTypes[0]
|
||||
|
||||
customFieldStore.currentCustomField.type = dataTypes[0].value
|
||||
selectedType.value = dataTypes[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomFieldData() {
|
||||
v$.value.currentCustomField.$touch()
|
||||
|
||||
if (v$.value.currentCustomField.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let data = {
|
||||
...customFieldStore.currentCustomField,
|
||||
}
|
||||
|
||||
if (customFieldStore.currentCustomField.options) {
|
||||
data.options = customFieldStore.currentCustomField.options.map(
|
||||
(option) => option.name
|
||||
)
|
||||
}
|
||||
|
||||
if (data.type == 'Time' && typeof data.default_answer == 'object') {
|
||||
let HH =
|
||||
data && data.default_answer && data.default_answer.HH
|
||||
? data.default_answer.HH
|
||||
: null
|
||||
let mm =
|
||||
data && data.default_answer && data.default_answer.mm
|
||||
? data.default_answer.mm
|
||||
: null
|
||||
let ss =
|
||||
data && data.default_answer && data.default_answer.ss
|
||||
? data.default_answer.ss
|
||||
: null
|
||||
|
||||
data.default_answer = `${HH}:${mm}`
|
||||
}
|
||||
|
||||
const action = customFieldStore.isEdit
|
||||
? customFieldStore.updateCustomField
|
||||
: customFieldStore.addCustomField
|
||||
|
||||
await action(data)
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
modalStore.refreshData ? modalStore.refreshData() : ''
|
||||
|
||||
closeCustomFieldModal()
|
||||
}
|
||||
|
||||
function addNewOption(option) {
|
||||
customFieldStore.currentCustomField.options = [
|
||||
{ name: option },
|
||||
...customFieldStore.currentCustomField.options,
|
||||
]
|
||||
}
|
||||
|
||||
function removeOption(index) {
|
||||
if (customFieldStore.isEdit && customFieldStore.currentCustomField.in_use) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = customFieldStore.currentCustomField.options[index]
|
||||
|
||||
if (option.name === customFieldStore.currentCustomField.default_answer) {
|
||||
customFieldStore.currentCustomField.default_answer = null
|
||||
}
|
||||
|
||||
customFieldStore.currentCustomField.options.splice(index, 1)
|
||||
}
|
||||
|
||||
function onChangeReset() {
|
||||
customFieldStore.$patch((state) => {
|
||||
state.currentCustomField.default_answer = null
|
||||
state.currentCustomField.is_required = false
|
||||
state.currentCustomField.placeholder = null
|
||||
state.currentCustomField.options = []
|
||||
})
|
||||
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function onSelectedTypeChange(data) {
|
||||
customFieldStore.currentCustomField.type = data.value
|
||||
}
|
||||
|
||||
function closeCustomFieldModal() {
|
||||
modalStore.closeModal()
|
||||
|
||||
setTimeout(() => {
|
||||
customFieldStore.resetCurrentCustomField()
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex items-center mt-1">
|
||||
<BaseInput
|
||||
v-model="option"
|
||||
type="text"
|
||||
class="w-full md:w-96"
|
||||
:placeholder="$t('settings.custom_fields.press_enter_to_add')"
|
||||
@click="onAddOption"
|
||||
@keydown.enter.prevent.stop="onAddOption"
|
||||
/>
|
||||
|
||||
<BaseIcon
|
||||
name="PlusCircleIcon"
|
||||
class="ml-1 text-primary-500 cursor-pointer"
|
||||
@click="onAddOption"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['onAdd'])
|
||||
|
||||
const option = ref(null)
|
||||
|
||||
function onAddOption() {
|
||||
if (option.value == null || option.value == '' || option.value == undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
emit('onAdd', option.value)
|
||||
|
||||
option.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.name.$error &&
|
||||
v$.doSpaceDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.doSpaceDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.doSpaceDiskConfig.name.$error"
|
||||
@input="v$.doSpaceDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.selected_driver.$error &&
|
||||
v$.doSpaceDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.doSpaceDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_root')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.root.$error &&
|
||||
v$.doSpaceDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.doSpaceDiskConfig.root.$error"
|
||||
@input="v$.doSpaceDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_key')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.key.$error &&
|
||||
v$.doSpaceDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.doSpaceDiskConfig.key.$error"
|
||||
@input="v$.doSpaceDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_secret')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.secret.$error &&
|
||||
v$.doSpaceDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.doSpaceDiskConfig.secret.$error"
|
||||
@input="v$.doSpaceDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_region')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.region.$error &&
|
||||
v$.doSpaceDiskConfig.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. nyc3"
|
||||
:invalid="v$.doSpaceDiskConfig.region.$error"
|
||||
@input="v$.doSpaceDiskConfig.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_endpoint')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.endpoint.$error &&
|
||||
v$.doSpaceDiskConfig.endpoint.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.endpoint"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. https://nyc3.digitaloceanspaces.com"
|
||||
:invalid="v$.doSpaceDiskConfig.endpoint.$error"
|
||||
@input="v$.doSpaceDiskConfig.endpoint.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.do_spaces_bucket')"
|
||||
:error="
|
||||
v$.doSpaceDiskConfig.bucket.$error &&
|
||||
v$.doSpaceDiskConfig.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.doSpaceDiskConfig.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. my-new-space"
|
||||
:invalid="v$.doSpaceDiskConfig.bucket.$error"
|
||||
@input="v$.doSpaceDiskConfig.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, url, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.doSpaceDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
doSpaceDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
endpoint: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
url: helpers.withMessage(t('validation.invalid_url'), url),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.doSpaceDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'doSpaces',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
endpoint: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'doSpaces',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(
|
||||
diskStore.doSpaceDiskConfig,
|
||||
JSON.parse(modalStore.data.credentials)
|
||||
)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.doSpaceDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'doSpaces')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.doSpaceDiskConfig.$touch()
|
||||
if (v$.value.doSpaceDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.doSpaceDiskConfig,
|
||||
name: diskStore.doSpaceDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.doSpaceDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
selected_disk,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.name.$error &&
|
||||
v$.dropBoxDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.dropBoxDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.name.$error"
|
||||
@input="v$.dropBoxDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.driver')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.selected_driver.$error &&
|
||||
v$.dropBoxDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.dropBoxDiskConfig.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_root')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.root.$error &&
|
||||
v$.dropBoxDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.dropBoxDiskConfig.root.$error"
|
||||
@input="v$.dropBoxDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_token')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.token.$error &&
|
||||
v$.dropBoxDiskConfig.token.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.token"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.token.$error"
|
||||
@input="v$.dropBoxDiskConfig.token.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_key')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.key.$error &&
|
||||
v$.dropBoxDiskConfig.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.dropBoxDiskConfig.key.$error"
|
||||
@input="v$.dropBoxDiskConfig.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_secret')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.secret.$error &&
|
||||
v$.dropBoxDiskConfig.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.dropBoxDiskConfig.secret.$error"
|
||||
@input="v$.dropBoxDiskConfig.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.dropbox_app')"
|
||||
:error="
|
||||
v$.dropBoxDiskConfig.app.$error &&
|
||||
v$.dropBoxDiskConfig.app.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.dropBoxDiskConfig.app"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.dropBoxDiskConfig.app.$error"
|
||||
@input="v$.dropBoxDiskConfig.app.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { reactive, ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let is_current_disk = ref(null)
|
||||
let selected_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.dropBoxDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
dropBoxDiskConfig: {
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
token: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
app: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.dropBoxDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'dropbox',
|
||||
token: null,
|
||||
key: null,
|
||||
secret: null,
|
||||
app: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 'dropbox',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.dropBoxDiskConfig, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.dropBoxDiskConfig, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'dropbox')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.dropBoxDiskConfig.$touch()
|
||||
if (v$.value.dropBoxDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
let data = {
|
||||
credentials: diskStore.dropBoxDiskConfig,
|
||||
name: diskStore.dropBoxDiskConfig.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.dropBoxDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
selected_driver,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
is_current_disk,
|
||||
selected_disk,
|
||||
isDisabled,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="submitData">
|
||||
<div class="px-4 sm:px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.localDiskConfig.name.$error &&
|
||||
v$.localDiskConfig.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.localDiskConfig.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.name.$error"
|
||||
@input="v$.localDiskConfig.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.localDiskConfig.selected_driver.$error &&
|
||||
v$.localDiskConfig.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
value-prop="value"
|
||||
:invalid="v$.localDiskConfig.selected_driver.$error"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.local_root')"
|
||||
:error="
|
||||
v$.localDiskConfig.root.$error &&
|
||||
v$.localDiskConfig.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.localDiskConfig.root"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.localDiskConfig.root.$error"
|
||||
placeholder="Ex./user/root/"
|
||||
@input="v$.localDiskConfig.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let isLoading = ref(false)
|
||||
let set_as_default = ref(false)
|
||||
let selected_disk = ref('')
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.localDiskConfig.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
localDiskConfig: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.localDiskConfig = {
|
||||
name: null,
|
||||
selected_driver: 'local',
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
|
||||
let data = reactive({
|
||||
disk: 'local',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.localDiskConfig, modalStore.data)
|
||||
diskStore.localDiskConfig.root = modalStore.data.credentials
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.localDiskConfig, diskData.data)
|
||||
}
|
||||
|
||||
selected_disk.value = props.disks.find((v) => v.value == 'local')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.localDiskConfig.$touch()
|
||||
|
||||
if (v$.value.localDiskConfig.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = reactive({
|
||||
credentials: diskStore.localDiskConfig.root,
|
||||
name: diskStore.localDiskConfig.name,
|
||||
driver: diskStore.localDiskConfig.selected_driver,
|
||||
set_as_default: set_as_default.value,
|
||||
})
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.localDiskConfig.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
selected_driver,
|
||||
selected_disk,
|
||||
isLoading,
|
||||
set_as_default,
|
||||
is_current_disk,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitData">
|
||||
<div class="px-8 py-6">
|
||||
<BaseInputGrid>
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.name')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.name.$error &&
|
||||
v$.s3DiskConfigData.name.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="diskStore.s3DiskConfigData.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:invalid="v$.s3DiskConfigData.name.$error"
|
||||
@input="v$.s3DiskConfigData.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$tc('settings.disk.driver')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.selected_driver.$error &&
|
||||
v$.s3DiskConfigData.selected_driver.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="selected_driver"
|
||||
:invalid="v$.s3DiskConfigData.selected_driver.$error"
|
||||
value-prop="value"
|
||||
:options="disks"
|
||||
searchable
|
||||
label="name"
|
||||
:can-deselect="false"
|
||||
@update:modelValue="onChangeDriver(data)"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_root')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.root.$error &&
|
||||
v$.s3DiskConfigData.root.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.root"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. /user/root/"
|
||||
:invalid="v$.s3DiskConfigData.root.$error"
|
||||
@input="v$.s3DiskConfigData.root.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_key')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.key.$error &&
|
||||
v$.s3DiskConfigData.key.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.key"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. KEIS4S39SERSDS"
|
||||
:invalid="v$.s3DiskConfigData.key.$error"
|
||||
@input="v$.s3DiskConfigData.key.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_secret')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.secret.$error &&
|
||||
v$.s3DiskConfigData.secret.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.secret"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. ********"
|
||||
:invalid="v$.s3DiskConfigData.secret.$error"
|
||||
@input="v$.s3DiskConfigData.secret.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_region')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.region.$error &&
|
||||
v$.s3DiskConfigData.region.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.region"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. us-west"
|
||||
:invalid="v$.s3DiskConfigData.region.$error"
|
||||
@input="v$.s3DiskConfigData.region.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.disk.aws_bucket')"
|
||||
:error="
|
||||
v$.s3DiskConfigData.bucket.$error &&
|
||||
v$.s3DiskConfigData.bucket.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model.trim="diskStore.s3DiskConfigData.bucket"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Ex. AppName"
|
||||
:invalid="v$.s3DiskConfigData.bucket.$error"
|
||||
@input="v$.s3DiskConfigData.bucket.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
<div v-if="!isDisabled" class="flex items-center mt-6">
|
||||
<div class="relative flex items-center w-12">
|
||||
<BaseSwitch v-model="set_as_default" class="flex" />
|
||||
</div>
|
||||
<div class="ml-4 right">
|
||||
<p class="p-0 mb-1 text-base leading-snug text-black box-title">
|
||||
{{ $t('settings.disk.is_default') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :disk-data="{ isLoading, submitData }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useDiskStore } from '@/scripts/admin/stores/disk'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers } from '@vuelidate/validators'
|
||||
export default {
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false,
|
||||
},
|
||||
disks: {
|
||||
type: Array,
|
||||
require: true,
|
||||
default: Array,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['submit', 'onChangeDisk'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const diskStore = useDiskStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
let set_as_default = ref(false)
|
||||
let isLoading = ref(false)
|
||||
let selected_disk = ref(null)
|
||||
let is_current_disk = ref(null)
|
||||
|
||||
const selected_driver = computed({
|
||||
get: () => diskStore.selected_driver,
|
||||
set: (value) => {
|
||||
diskStore.selected_driver = value
|
||||
diskStore.s3DiskConfigData.selected_driver = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
s3DiskConfigData: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
root: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
key: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
secret: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
region: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
bucket: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
selected_driver: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => diskStore)
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
diskStore.s3DiskConfigData = {
|
||||
name: null,
|
||||
selected_driver: 's3',
|
||||
key: null,
|
||||
secret: null,
|
||||
region: null,
|
||||
bucket: null,
|
||||
root: null,
|
||||
}
|
||||
})
|
||||
|
||||
loadData()
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
let data = reactive({
|
||||
disk: 's3',
|
||||
})
|
||||
|
||||
if (props.isEdit) {
|
||||
Object.assign(diskStore.s3DiskConfigData, modalStore.data)
|
||||
set_as_default.value = modalStore.data.set_as_default
|
||||
|
||||
if (set_as_default.value) {
|
||||
is_current_disk.value = true
|
||||
}
|
||||
} else {
|
||||
let diskData = await diskStore.fetchDiskEnv(data)
|
||||
Object.assign(diskStore.s3DiskConfigData, diskData.data)
|
||||
}
|
||||
selected_disk.value = props.disks.find((v) => v.value == 's3')
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.isEdit && set_as_default.value && is_current_disk.value
|
||||
? true
|
||||
: false
|
||||
})
|
||||
|
||||
async function submitData() {
|
||||
v$.value.s3DiskConfigData.$touch()
|
||||
if (v$.value.s3DiskConfigData.$invalid) {
|
||||
return true
|
||||
}
|
||||
|
||||
let data = {
|
||||
credentials: diskStore.s3DiskConfigData,
|
||||
name: diskStore.s3DiskConfigData.name,
|
||||
driver: selected_disk.value.value,
|
||||
set_as_default: set_as_default.value,
|
||||
}
|
||||
|
||||
emit('submit', data)
|
||||
return false
|
||||
}
|
||||
|
||||
function onChangeDriver() {
|
||||
emit('onChangeDisk', diskStore.s3DiskConfigData.selected_driver)
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
diskStore,
|
||||
modalStore,
|
||||
set_as_default,
|
||||
isLoading,
|
||||
selected_disk,
|
||||
selected_driver,
|
||||
is_current_disk,
|
||||
loadData,
|
||||
submitData,
|
||||
onChangeDriver,
|
||||
isDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
88
resources/scripts/admin/layouts/LayoutBasic.vue
Normal file
88
resources/scripts/admin/layouts/LayoutBasic.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div v-if="isAppLoaded" class="h-full">
|
||||
<NotificationRoot />
|
||||
|
||||
<SiteHeader />
|
||||
|
||||
<SiteSidebar />
|
||||
|
||||
<ExchangeRateBulkUpdateModal />
|
||||
|
||||
<main
|
||||
class="
|
||||
pt-16
|
||||
pb-16
|
||||
h-screen h-screen-ios
|
||||
overflow-y-auto
|
||||
md:pl-56
|
||||
xl:pl-64
|
||||
min-h-0
|
||||
"
|
||||
>
|
||||
<router-view />
|
||||
</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'
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
globalStore.bootstrap().then((res) => {
|
||||
if (route.meta.ability && !userStore.hasAbilities(route.meta.ability)) {
|
||||
router.push({ name: 'account.settings' })
|
||||
} 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>
|
||||
13
resources/scripts/admin/layouts/LayoutInstallation.vue
Normal file
13
resources/scripts/admin/layouts/LayoutInstallation.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="h-screen h-screen-ios 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>
|
||||
142
resources/scripts/admin/layouts/LayoutLogin.vue
Normal file
142
resources/scripts/admin/layouts/LayoutLogin.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
grid
|
||||
h-screen h-screen-ios
|
||||
grid-cols-12
|
||||
overflow-y-hidden
|
||||
bg-gray-100
|
||||
"
|
||||
>
|
||||
<NotificationRoot />
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
max-w-sm
|
||||
col-span-12
|
||||
p-4
|
||||
mx-auto
|
||||
text-gray-900
|
||||
md:p-8 md:col-span-6
|
||||
lg:col-span-4
|
||||
flex-2
|
||||
md:pb-48 md:pt-40
|
||||
"
|
||||
>
|
||||
<div class="w-full">
|
||||
<MainLogo 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-gray-400
|
||||
md:pt-40
|
||||
"
|
||||
>
|
||||
<p class="mb-3">
|
||||
Copyright @ Crater Invoice, Inc. {{ new Date().getFullYear() }}
|
||||
</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="pl-20 xl:pl-0 relative z-50">
|
||||
<h1
|
||||
class="
|
||||
hidden
|
||||
mb-3
|
||||
text-3xl
|
||||
leading-normal
|
||||
text-left text-white
|
||||
xl:text-5xl xl:leading-tight
|
||||
md:none
|
||||
lg:block
|
||||
"
|
||||
>
|
||||
<b class="font-bold">Simple Invoicing</b> <br />
|
||||
for Individuals & <br />
|
||||
Small Businesses <br />
|
||||
</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
|
||||
"
|
||||
>
|
||||
Crater helps you track expenses, record payments & generate beautiful
|
||||
<br />
|
||||
invoices & estimates. <br />
|
||||
</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'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
219
resources/scripts/admin/layouts/partials/TheSiteHeader.vue
Normal file
219
resources/scripts/admin/layouts/partials/TheSiteHeader.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<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-gradient-to-r
|
||||
from-primary-500
|
||||
to-primary-400
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
to="/admin/dashboard"
|
||||
class="
|
||||
float-none
|
||||
text-lg
|
||||
not-italic
|
||||
font-black
|
||||
tracking-wider
|
||||
text-white
|
||||
brand-main
|
||||
md:float-left
|
||||
font-base
|
||||
hidden
|
||||
md:block
|
||||
"
|
||||
>
|
||||
<MainLogo class="h-6" light-color="white" dark-color="white" />
|
||||
</router-link>
|
||||
|
||||
<!-- toggle button-->
|
||||
<div
|
||||
:class="{ 'is-active': globalStore.isSidebarOpen }"
|
||||
class="
|
||||
flex
|
||||
float-left
|
||||
p-1
|
||||
overflow-visible
|
||||
text-sm
|
||||
ease-linear
|
||||
bg-white
|
||||
border-0
|
||||
rounded
|
||||
cursor-pointer
|
||||
md:hidden md:ml-0
|
||||
hover:bg-gray-100
|
||||
"
|
||||
@click.prevent="onToggle"
|
||||
>
|
||||
<BaseIcon name="MenuIcon" class="!w-6 !h-6 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<ul class="flex float-right h-8 m-0 list-none md:h-9">
|
||||
<li
|
||||
v-if="hasCreateAbilities"
|
||||
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-black
|
||||
bg-white
|
||||
rounded
|
||||
md:h-9 md:w-9
|
||||
"
|
||||
>
|
||||
<BaseIcon name="PlusIcon" class="w-5 h-5 text-gray-600" />
|
||||
</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-gray-400 group-hover:text-gray-500"
|
||||
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-gray-400 group-hover:text-gray-500"
|
||||
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-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('customers.new_customer') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
</BaseDropdown>
|
||||
</li>
|
||||
|
||||
<li 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 md:h-9 md:w-9"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<router-link to="/admin/settings/account-settings">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="CogIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('navigation.settings') }}
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<BaseDropdownItem @click="logout">
|
||||
<BaseIcon
|
||||
name="LogoutIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
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 } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
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 router = useRouter()
|
||||
|
||||
const previewAvatar = computed(() => {
|
||||
return userStore.currentUser && userStore.currentUser.avatar !== 0
|
||||
? userStore.currentUser.avatar
|
||||
: getDefaultAvatar()
|
||||
})
|
||||
|
||||
function getDefaultAvatar() {
|
||||
const imgUrl = new URL('/img/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)
|
||||
}
|
||||
</script>
|
||||
179
resources/scripts/admin/layouts/partials/TheSiteSidebar.vue
Normal file
179
resources/scripts/admin/layouts/partials/TheSiteSidebar.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<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 bg-opacity-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-white">
|
||||
<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-none
|
||||
focus:ring-2
|
||||
focus:ring-inset
|
||||
focus:ring-white
|
||||
"
|
||||
@click="globalStore.setSidebarVisibility(false)"
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<BaseIcon
|
||||
name="XIcon"
|
||||
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="Crater Logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
v-for="menu in globalStore.menuGroups"
|
||||
:key="menu"
|
||||
class="mt-5 space-y-1"
|
||||
>
|
||||
<router-link
|
||||
v-for="item in menu"
|
||||
:key="item.name"
|
||||
:to="item.link"
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500 border-primary-500 bg-gray-100 '
|
||||
: 'text-black',
|
||||
'cursor-pointer px-0 pl-4 py-3 border-transparent flex items-center border-l-4 border-solid text-sm not-italic font-medium',
|
||||
]"
|
||||
@click="globalStore.setSidebarVisibility(false)"
|
||||
>
|
||||
<BaseIcon
|
||||
:name="item.icon"
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500 '
|
||||
: 'text-gray-400',
|
||||
'mr-4 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="
|
||||
hidden
|
||||
w-56
|
||||
h-screen h-screen-ios
|
||||
pb-32
|
||||
overflow-y-auto
|
||||
bg-white
|
||||
border-r border-gray-200 border-solid
|
||||
xl:w-64
|
||||
md:fixed md:flex md:flex-col md:inset-y-0
|
||||
pt-16
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="menu in globalStore.menuGroups"
|
||||
:key="menu"
|
||||
class="p-0 m-0 mt-6 list-none"
|
||||
>
|
||||
<router-link
|
||||
v-for="item in menu"
|
||||
:key="item"
|
||||
:to="item.link"
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500 border-primary-500 bg-gray-100 '
|
||||
: 'text-black',
|
||||
'cursor-pointer px-0 pl-6 hover:bg-gray-50 py-3 group flex items-center border-l-4 border-solid border-transparent text-sm not-italic font-medium',
|
||||
]"
|
||||
>
|
||||
<BaseIcon
|
||||
:name="item.icon"
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500 group-hover:text-primary-500 '
|
||||
: 'text-gray-400 group-hover:text-black',
|
||||
'mr-4 shrink-0 h-5 w-5 ',
|
||||
]"
|
||||
/>
|
||||
|
||||
{{ $t(item.title) }}
|
||||
</router-link>
|
||||
</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>
|
||||
70
resources/scripts/admin/stores/auth.js
Normal file
70
resources/scripts/admin/stores/auth.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'auth',
|
||||
state: () => ({
|
||||
status: '',
|
||||
|
||||
loginData: {
|
||||
email: '',
|
||||
password: '',
|
||||
remember: '',
|
||||
},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
login(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get('/sanctum/csrf-cookie').then((response) => {
|
||||
if (response) {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get('/auth/logout')
|
||||
.then((response) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'Logged out successfully.',
|
||||
})
|
||||
|
||||
window.router.push('/login')
|
||||
// resetStore.clearPinia()
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
window.router.push('/')
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
76
resources/scripts/admin/stores/backup.js
Normal file
76
resources/scripts/admin/stores/backup.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'backup',
|
||||
|
||||
state: () => ({
|
||||
backups: [],
|
||||
currentBackupData: {
|
||||
option: 'full',
|
||||
selected_disk: null,
|
||||
},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
fetchBackups(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
128
resources/scripts/admin/stores/category.js
Normal file
128
resources/scripts/admin/stores/category.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'category',
|
||||
|
||||
state: () => ({
|
||||
categories: [],
|
||||
currentCategory: {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isEdit: (state) => (state.currentCategory.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchCategories(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
189
resources/scripts/admin/stores/company.js
Normal file
189
resources/scripts/admin/stores/company.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'company',
|
||||
|
||||
state: () => ({
|
||||
companies: [],
|
||||
selectedCompany: null,
|
||||
selectedCompanySettings: {},
|
||||
selectedCompanyCurrency: null,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setSelectedCompany(data) {
|
||||
window.Ls.set('selectedCompany', data.id)
|
||||
this.selectedCompany = data
|
||||
},
|
||||
|
||||
fetchBasicMailConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get('/api/v1/company/mail/config')
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateCompany(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateCompanyLogo(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/company/upload-logo', data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addNewCompany(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get('/api/v1/companies')
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchCompanySettings(settings) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.post(`/api/v1/companies/delete`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
setDefaultCurrency(data) {
|
||||
this.defaultCurrency = data.currency
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
211
resources/scripts/admin/stores/custom-field.js
Normal file
211
resources/scripts/admin/stores/custom-field.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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) => {
|
||||
axios
|
||||
.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
|
||||
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
256
resources/scripts/admin/stores/customer.js
Normal file
256
resources/scripts/admin/stores/customer.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'customer',
|
||||
state: () => ({
|
||||
customers: [],
|
||||
totalCustomers: 0,
|
||||
selectAllField: false,
|
||||
selectedCustomers: [],
|
||||
selectedViewCustomer: {},
|
||||
isFetchingInitialSettings: false,
|
||||
isFetchingViewData: false,
|
||||
currentCustomer: {
|
||||
...customerStub(),
|
||||
},
|
||||
}),
|
||||
|
||||
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) => {
|
||||
axios
|
||||
.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
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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.tc('customers.deleted_message', 1),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultipleCustomers() {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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.tc('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 }
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
87
resources/scripts/admin/stores/dashboard.js
Normal file
87
resources/scripts/admin/stores/dashboard.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
183
resources/scripts/admin/stores/disk.js
Normal file
183
resources/scripts/admin/stores/disk.js
Normal file
@@ -0,0 +1,183 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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: '',
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDiskDrivers: (state) => state.diskDrivers,
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchDiskEnv(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/disks/${data.disk}`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchDisks(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
563
resources/scripts/admin/stores/estimate.js
Normal file
563
resources/scripts/admin/stores/estimate.js
Normal file
@@ -0,0 +1,563 @@
|
||||
import axios from 'axios'
|
||||
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'
|
||||
|
||||
export const useEstimateStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: '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)
|
||||
},
|
||||
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() {
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
|
||||
isEdit: (state) => (state.newEstimate.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
resetCurrentEstimate() {
|
||||
this.newEstimate = {
|
||||
...estimateStub(),
|
||||
}
|
||||
},
|
||||
|
||||
previewEstimate(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/estimates/${id}`)
|
||||
.then((response) => {
|
||||
Object.assign(this.newEstimate, response.data.data)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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.tc('estimates.deleted_message', 2),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateEstimate(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
markAsAccepted(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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()
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
this.addSalesTaxUs()
|
||||
}
|
||||
this.isFetchingInitialSettings = false
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
this.isFetchingInitialSettings = false
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
249
resources/scripts/admin/stores/exchange-rate.js
Normal file
249
resources/scripts/admin/stores/exchange-rate.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/currencies/${currencyId}/exchange-rate`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
getCurrencyConverterServers() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/currencies/${currency_id}/active-provider`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
239
resources/scripts/admin/stores/expense.js
Normal file
239
resources/scripts/admin/stores/expense.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/expenses/${id}`)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
Object.assign(this.currentExpense, response.data.data)
|
||||
this.currentExpense.selectedCurrency =
|
||||
response.data.data.currency
|
||||
|
||||
if (response.data.data.attachment_receipt) {
|
||||
if (
|
||||
utils.isImageFile(
|
||||
response.data.data.attachment_receipt_meta.mime_type
|
||||
)
|
||||
) {
|
||||
this.currentExpense.receiptFiles = [
|
||||
{ image: `/expenses/${id}/receipt` },
|
||||
]
|
||||
} 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) => {
|
||||
axios
|
||||
.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 }) {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const formData = utils.toFormData(data)
|
||||
|
||||
formData.append('_method', 'PUT')
|
||||
|
||||
return new Promise((resolve) => {
|
||||
axios.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) => {
|
||||
axios
|
||||
.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.tc('expenses.deleted_message', 1),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultipleExpenses() {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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.tc('expenses.deleted_message', 2),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchPaymentModes(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payment-methods`, { params })
|
||||
.then((response) => {
|
||||
this.paymentModes = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
243
resources/scripts/admin/stores/global.js
Normal file
243
resources/scripts/admin/stores/global.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'global',
|
||||
state: () => ({
|
||||
// Global Configuration
|
||||
config: null,
|
||||
globalSettings: null,
|
||||
|
||||
// Global Lists
|
||||
timeZones: [],
|
||||
dateFormats: [],
|
||||
currencies: [],
|
||||
countries: [],
|
||||
languages: [],
|
||||
fiscalYears: [],
|
||||
|
||||
// Menus
|
||||
mainMenu: [],
|
||||
settingMenu: [],
|
||||
|
||||
// Boolean Flags
|
||||
isAppLoaded: false,
|
||||
isSidebarOpen: false,
|
||||
areCurrenciesLoading: false,
|
||||
|
||||
downloadReport: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
menuGroups: (state) => {
|
||||
return Object.values(_.groupBy(state.mainMenu, 'group'))
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
bootstrap() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get('/api/v1/bootstrap')
|
||||
.then((response) => {
|
||||
const companyStore = useCompanyStore()
|
||||
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
|
||||
|
||||
// company store
|
||||
companyStore.companies = response.data.companies
|
||||
companyStore.selectedCompany = response.data.current_company
|
||||
companyStore.setSelectedCompany(response.data.current_company)
|
||||
companyStore.selectedCompanySettings =
|
||||
response.data.current_company_settings
|
||||
companyStore.selectedCompanyCurrency =
|
||||
response.data.current_company_currency
|
||||
|
||||
global.locale =
|
||||
response.data.current_user_settings.language || 'en'
|
||||
|
||||
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
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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 {
|
||||
axios
|
||||
.get('/api/v1/date/formats')
|
||||
.then((response) => {
|
||||
this.dateFormats = response.data.date_formats
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
fetchTimeZones() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.timeZones.length) {
|
||||
resolve(this.timeZones)
|
||||
} else {
|
||||
axios
|
||||
.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 {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/number-placeholders`, { params })
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
setSidebarVisibility(val) {
|
||||
this.isSidebarOpen = val
|
||||
},
|
||||
|
||||
setIsAppLoaded(isAppLoaded) {
|
||||
this.isAppLoaded = isAppLoaded
|
||||
},
|
||||
|
||||
updateGlobalSettings({ data, message }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
172
resources/scripts/admin/stores/installation.js
Normal file
172
resources/scripts/admin/stores/installation.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: 'installation',
|
||||
|
||||
state: () => ({
|
||||
currentDataBaseData: {
|
||||
database_connection: 'mysql',
|
||||
database_hostname: '127.0.0.1',
|
||||
database_port: '3306',
|
||||
database_name: null,
|
||||
database_username: null,
|
||||
database_password: null,
|
||||
app_url: window.location.origin,
|
||||
},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
fetchInstallationRequirements() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/installation/requirements`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchInstallationStep() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/installation/wizard-step`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addInstallationStep(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/installation/wizard-step`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchInstallationPermissions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/installation/permissions`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchInstallationDatabase(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/installation/database/config`, { params })
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addInstallationDatabase(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/installation/database/config`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addInstallationFinish() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/installation/finish`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
setInstallationDomain(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/installation/set-domain`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
installationLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get('/sanctum/csrf-cookie').then((response) => {
|
||||
if (response) {
|
||||
axios
|
||||
.post('/api/v1/installation/login')
|
||||
.then((response) => {
|
||||
companyStore.setSelectedCompany(response.data.company)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
checkAutheticated() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/auth/check`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
513
resources/scripts/admin/stores/invoice.js
Normal file
513
resources/scripts/admin/stores/invoice.js
Normal file
@@ -0,0 +1,513 @@
|
||||
import axios from 'axios'
|
||||
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'
|
||||
|
||||
export const useInvoiceStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return defineStoreFunc({
|
||||
id: '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)
|
||||
},
|
||||
|
||||
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() {
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
|
||||
isEdit: (state) => (state.newInvoice.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
resetCurrentInvoice() {
|
||||
this.newInvoice = {
|
||||
...invoiceStub(),
|
||||
}
|
||||
},
|
||||
|
||||
previewInvoice(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.get(`/api/v1/invoices/${id}`)
|
||||
.then((response) => {
|
||||
Object.assign(this.newInvoice, response.data.data)
|
||||
this.newInvoice.customer = response.data.data.customer
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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.tc('invoices.deleted_message', 2),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateInvoice(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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()
|
||||
|
||||
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) {
|
||||
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
|
||||
this.newInvoice.invoice_date = moment().format('YYYY-MM-DD')
|
||||
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)
|
||||
}
|
||||
}
|
||||
if (isEdit) {
|
||||
this.addSalesTaxUs()
|
||||
}
|
||||
|
||||
this.isFetchingInitialSettings = false
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
336
resources/scripts/admin/stores/item.js
Normal file
336
resources/scripts/admin/stores/item.js
Normal file
@@ -0,0 +1,336 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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.tc('items.deleted_message', 1),
|
||||
})
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultipleItems() {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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.tc('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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
146
resources/scripts/admin/stores/mail-driver.js
Normal file
146
resources/scripts/admin/stores/mail-driver.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import axios from 'axios'
|
||||
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({
|
||||
id: '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_encryption: 'tls',
|
||||
from_mail: '',
|
||||
from_name: '',
|
||||
},
|
||||
|
||||
smtpConfig: {
|
||||
mail_driver: '',
|
||||
mail_host: '',
|
||||
mail_port: null,
|
||||
mail_username: '',
|
||||
mail_password: '',
|
||||
mail_encryption: 'tls',
|
||||
from_mail: '',
|
||||
from_name: '',
|
||||
},
|
||||
}),
|
||||
|
||||
actions: {
|
||||
fetchMailDrivers() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.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)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
132
resources/scripts/admin/stores/module.js
Normal file
132
resources/scripts/admin/stores/module.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
|
||||
export const useModuleStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'modules',
|
||||
|
||||
state: () => ({
|
||||
currentModule: {},
|
||||
modules: [],
|
||||
apiToken: null,
|
||||
currentUser: {
|
||||
api_token: null,
|
||||
},
|
||||
enableModules: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
salesTaxUSEnabled: (state) => (state.enableModules.includes('SalesTaxUS')),
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchModules(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/modules`)
|
||||
.then((response) => {
|
||||
this.modules = response.data.data
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchModule(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/modules/${id}`)
|
||||
.then((response) => {
|
||||
if (response.data.error === 'invalid_token') {
|
||||
this.currentModule = {},
|
||||
this.modules = [],
|
||||
this.apiToken = null,
|
||||
this.currentUser.api_token = null,
|
||||
window.router.push('/admin/modules')
|
||||
} else {
|
||||
this.currentModule = response.data
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
checkApiToken(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/modules/check?api_token=${token}`)
|
||||
.then((response) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
if (response.data.error === 'invalid_token') {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: global.t('modules.invalid_api_token'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
disableModule(module) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/modules/${module}/disable`)
|
||||
.then((response) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('modules.module_disabled'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
enableModule(module) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/modules/${module}/enable`)
|
||||
.then((response) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('modules.module_enabled'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
117
resources/scripts/admin/stores/note.js
Normal file
117
resources/scripts/admin/stores/note.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const useNotesStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'notes',
|
||||
|
||||
state: () => ({
|
||||
notes: [],
|
||||
currentNote: {
|
||||
id: null,
|
||||
type: '',
|
||||
name: '',
|
||||
notes: '',
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isEdit: (state) => (state.currentNote.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
resetCurrentNote() {
|
||||
this.currentNote = {
|
||||
type: '',
|
||||
name: '',
|
||||
notes: '',
|
||||
}
|
||||
},
|
||||
|
||||
fetchNotes(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/notes`, { params })
|
||||
.then((response) => {
|
||||
this.notes = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchNote(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/notes/${id}`)
|
||||
.then((response) => {
|
||||
this.currentNote = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addNote(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/notes', data)
|
||||
.then((response) => {
|
||||
this.notes.push(response.data)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateNote(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/notes/${data.id}`, data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.notes.findIndex(
|
||||
(notes) => notes.id === response.data.data.id
|
||||
)
|
||||
this.notes[pos] = data.notes
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteNote(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.delete(`/api/v1/notes/${id}`)
|
||||
.then((response) => {
|
||||
let index = this.notes.findIndex((note) => note.id === id)
|
||||
this.notes.splice(index, 1)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
429
resources/scripts/admin/stores/payment.js
Normal file
429
resources/scripts/admin/stores/payment.js
Normal file
@@ -0,0 +1,429 @@
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useCompanyStore } from './company'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import paymentStub from '../stub/payment'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const usePaymentStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'payment',
|
||||
|
||||
state: () => ({
|
||||
payments: [],
|
||||
paymentTotalCount: 0,
|
||||
|
||||
selectAllField: false,
|
||||
selectedPayments: [],
|
||||
selectedNote: null,
|
||||
showExchangeRate: false,
|
||||
drivers: [],
|
||||
providers: [],
|
||||
|
||||
paymentProviders: {
|
||||
id: null,
|
||||
name: '',
|
||||
driver: '',
|
||||
active: false,
|
||||
settings: {
|
||||
key: '',
|
||||
secret: '',
|
||||
},
|
||||
},
|
||||
|
||||
currentPayment: {
|
||||
...paymentStub,
|
||||
},
|
||||
|
||||
paymentModes: [],
|
||||
currentPaymentMode: {
|
||||
id: '',
|
||||
name: null,
|
||||
},
|
||||
|
||||
isFetchingInitialData: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isEdit: (state) => (state.paymentProviders.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchPaymentInitialData(isEdit) {
|
||||
const companyStore = useCompanyStore()
|
||||
const route = useRoute()
|
||||
|
||||
this.isFetchingInitialData = true
|
||||
|
||||
let actions = []
|
||||
if (isEdit) {
|
||||
actions = [this.fetchPayment(route.params.id)]
|
||||
}
|
||||
Promise.all([
|
||||
this.fetchPaymentModes({ limit: 'all' }),
|
||||
this.getNextNumber(),
|
||||
...actions,
|
||||
])
|
||||
.then(async ([res1, res2, res3]) => {
|
||||
if (isEdit) {
|
||||
if (res3.data.data.invoice) {
|
||||
this.currentPayment.maxPayableAmount = parseInt(
|
||||
res3.data.data.invoice.due_amount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// On Create
|
||||
else if (!isEdit && res2.data) {
|
||||
this.currentPayment.payment_date = moment().format('YYYY-MM-DD')
|
||||
this.currentPayment.payment_number = res2.data.nextNumber
|
||||
this.currentPayment.currency =
|
||||
companyStore.selectedCompanyCurrency
|
||||
}
|
||||
|
||||
this.isFetchingInitialData = false
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
fetchPayments(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payments`, { params })
|
||||
.then((response) => {
|
||||
this.payments = response.data.data
|
||||
this.paymentTotalCount = response.data.meta.payment_total_count
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchPayment(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payments/${id}`)
|
||||
.then((response) => {
|
||||
Object.assign(this.currentPayment, response.data.data)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addPayment(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/payments', data)
|
||||
.then((response) => {
|
||||
this.payments.push(response.data)
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('payments.created_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updatePayment(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/payments/${data.id}`, data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.payments.findIndex(
|
||||
(payment) => payment.id === response.data.data.id
|
||||
)
|
||||
|
||||
this.payments[pos] = data.payment
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('payments.updated_message'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deletePayment(id) {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/payments/delete`, id)
|
||||
.then((response) => {
|
||||
let index = this.payments.findIndex(
|
||||
(payment) => payment.id === id
|
||||
)
|
||||
this.payments.splice(index, 1)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('payments.deleted_message', 1),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultiplePayments() {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/payments/delete`, { ids: this.selectedPayments })
|
||||
.then((response) => {
|
||||
this.selectedPayments.forEach((payment) => {
|
||||
let index = this.payments.findIndex(
|
||||
(_payment) => _payment.id === payment.id
|
||||
)
|
||||
this.payments.splice(index, 1)
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.tc('payments.deleted_message', 2),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
setSelectAllState(data) {
|
||||
this.selectAllField = data
|
||||
},
|
||||
|
||||
selectPayment(data) {
|
||||
this.selectedPayments = data
|
||||
if (this.selectedPayments.length === this.payments.length) {
|
||||
this.selectAllField = true
|
||||
} else {
|
||||
this.selectAllField = false
|
||||
}
|
||||
},
|
||||
|
||||
selectAllPayments() {
|
||||
if (this.selectedPayments.length === this.payments.length) {
|
||||
this.selectedPayments = []
|
||||
this.selectAllField = false
|
||||
} else {
|
||||
let allPaymentIds = this.payments.map((payment) => payment.id)
|
||||
this.selectedPayments = allPaymentIds
|
||||
this.selectAllField = true
|
||||
}
|
||||
},
|
||||
|
||||
selectNote(data) {
|
||||
this.selectedNote = null
|
||||
this.selectedNote = data
|
||||
},
|
||||
|
||||
resetSelectedNote(data) {
|
||||
this.selectedNote = null
|
||||
},
|
||||
|
||||
searchPayment(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payments`, { params })
|
||||
.then((response) => {
|
||||
this.payments = response.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
previewPayment(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payments/${params.id}/send/preview`, { params })
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
sendEmail(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/payments/${data.id}/send`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
getNextNumber(params, setState = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/next-number?key=payment`, { params })
|
||||
.then((response) => {
|
||||
if (setState) {
|
||||
this.currentPayment.payment_number = response.data.nextNumber
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
resetCurrentPayment() {
|
||||
this.currentPayment = {
|
||||
...paymentStub,
|
||||
}
|
||||
},
|
||||
|
||||
fetchPaymentModes(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payment-methods`, { params })
|
||||
.then((response) => {
|
||||
this.paymentModes = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchPaymentMode(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/payment-methods/${id}`)
|
||||
.then((response) => {
|
||||
this.currentPaymentMode = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addPaymentMode(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/payment-methods`, data)
|
||||
.then((response) => {
|
||||
this.paymentModes.push(response.data.data)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.payment_modes.payment_mode_added'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updatePaymentMode(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/payment-methods/${data.id}`, data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.paymentModes.findIndex(
|
||||
(paymentMode) => paymentMode.id === response.data.data.id
|
||||
)
|
||||
this.paymentModes[pos] = data.paymentModes
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t(
|
||||
'settings.payment_modes.payment_mode_updated'
|
||||
),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deletePaymentMode(id) {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.delete(`/api/v1/payment-methods/${id}`)
|
||||
.then((response) => {
|
||||
let index = this.paymentModes.findIndex(
|
||||
(paymentMode) => paymentMode.id === id
|
||||
)
|
||||
this.paymentModes.splice(index, 1)
|
||||
if (response.data.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.payment_modes.deleted_message'),
|
||||
})
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
460
resources/scripts/admin/stores/recurring-invoice.js
Normal file
460
resources/scripts/admin/stores/recurring-invoice.js
Normal file
@@ -0,0 +1,460 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import recurringInvoiceStub from '@/scripts/admin/stub/recurring-invoice'
|
||||
import recurringInvoiceItemStub from '@/scripts/admin/stub/recurring-invoice-item'
|
||||
import TaxStub from '../stub/tax'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useCompanyStore } from './company'
|
||||
import { useItemStore } from './item'
|
||||
import { useTaxTypeStore } from './tax-type'
|
||||
import { useCustomerStore } from './customer'
|
||||
import Guid from 'guid'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import { useInvoiceStore } from './invoice'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
|
||||
export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'recurring-invoice',
|
||||
|
||||
state: () => ({
|
||||
templates: [],
|
||||
recurringInvoices: [],
|
||||
selectedRecurringInvoices: [],
|
||||
totalRecurringInvoices: 0,
|
||||
isFetchingInitialSettings: false,
|
||||
isFetchingViewData: false,
|
||||
showExchangeRate: false,
|
||||
selectAllField: false,
|
||||
newRecurringInvoice: {
|
||||
...recurringInvoiceStub(),
|
||||
},
|
||||
|
||||
frequencies: [
|
||||
{ label: 'Every Minute', value: '* * * * *' },
|
||||
{ label: 'Every 30 Minute', value: '*/30 * * * *' },
|
||||
{ label: 'Every Hour', value: '0 * * * *' },
|
||||
{ label: 'Every 2 Hour', value: '0 */2 * * *' },
|
||||
{ label: 'Twice A Day', value: '0 13-15 * * *' },
|
||||
{ label: 'Every Week', value: '0 0 * * 0' },
|
||||
{ label: 'Every 15 Days', value: '0 5 */15 * *' },
|
||||
{ label: 'First Day Of Month', value: '0 0 1 * *' },
|
||||
{ label: 'Every 6 Month', value: '0 0 1 */6 *' },
|
||||
{ label: 'Every Year', value: '0 0 1 1 *' },
|
||||
{ label: 'Custom', value: 'CUSTOM' },
|
||||
],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getSubTotal() {
|
||||
return (
|
||||
this.newRecurringInvoice?.items.reduce(function (a, b) {
|
||||
return a + b['total']
|
||||
}, 0) || 0
|
||||
)
|
||||
},
|
||||
|
||||
getTotalSimpleTax() {
|
||||
return _.sumBy(this.newRecurringInvoice.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
},
|
||||
|
||||
getTotalCompoundTax() {
|
||||
return _.sumBy(this.newRecurringInvoice.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
},
|
||||
|
||||
getTotalTax() {
|
||||
if (
|
||||
this.newRecurringInvoice.tax_per_item === 'NO' ||
|
||||
this.newRecurringInvoice.tax_per_item === null
|
||||
) {
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
}
|
||||
return _.sumBy(this.newRecurringInvoice.items, function (tax) {
|
||||
return tax.tax
|
||||
})
|
||||
},
|
||||
|
||||
getSubtotalWithDiscount() {
|
||||
return this.getSubTotal - this.newRecurringInvoice.discount_val
|
||||
},
|
||||
|
||||
getTotal() {
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
resetCurrentRecurringInvoice() {
|
||||
this.newRecurringInvoice = {
|
||||
...recurringInvoiceStub(),
|
||||
}
|
||||
},
|
||||
|
||||
deselectItem(index) {
|
||||
this.newRecurringInvoice.items[index] = {
|
||||
...recurringInvoiceItemStub,
|
||||
id: Guid.raw(),
|
||||
taxes: [{ ...TaxStub, id: Guid.raw() }],
|
||||
}
|
||||
},
|
||||
|
||||
addRecurringInvoice(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/recurring-invoices', data)
|
||||
.then((response) => {
|
||||
this.recurringInvoices = [
|
||||
...this.recurringInvoices,
|
||||
response.data.recurringInvoice,
|
||||
]
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('recurring_invoices.created_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchRecurringInvoice(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.isFetchingViewData = true
|
||||
axios
|
||||
.get(`/api/v1/recurring-invoices/${id}`)
|
||||
.then((response) => {
|
||||
Object.assign(this.newRecurringInvoice, response.data.data)
|
||||
this.newRecurringInvoice.invoices =
|
||||
response.data.data.invoices || []
|
||||
this.setSelectedFrequency()
|
||||
this.isFetchingViewData = false
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isFetchingViewData = false
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateRecurringInvoice(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/recurring-invoices/${data.id}`, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('recurring_invoices.updated_message'),
|
||||
})
|
||||
|
||||
let pos = this.recurringInvoices.findIndex(
|
||||
(invoice) => invoice.id === response.data.data.id
|
||||
)
|
||||
|
||||
this.recurringInvoices[pos] = response.data.data
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
selectCustomer(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/customers/${id}`)
|
||||
.then((response) => {
|
||||
this.newRecurringInvoice.customer = response.data.data
|
||||
this.newRecurringInvoice.customer_id = response.data.data.id
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
searchRecurringInvoice(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/recurring-invoices?${data}`)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchRecurringInvoices(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/recurring-invoices`, { params })
|
||||
.then((response) => {
|
||||
this.recurringInvoices = response.data.data
|
||||
this.totalRecurringInvoices =
|
||||
response.data.meta.recurring_invoice_total_count
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteRecurringInvoice(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/recurring-invoices/delete`, id)
|
||||
.then((response) => {
|
||||
let index = this.recurringInvoices.findIndex(
|
||||
(invoice) => invoice.id === id
|
||||
)
|
||||
this.recurringInvoices.splice(index, 1)
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultipleRecurringInvoices(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let ids = this.selectedRecurringInvoices
|
||||
if (id) {
|
||||
ids = [id]
|
||||
}
|
||||
axios
|
||||
.post(`/api/v1/recurring-invoices/delete`, {
|
||||
ids: ids,
|
||||
})
|
||||
.then((response) => {
|
||||
this.selectedRecurringInvoices.forEach((invoice) => {
|
||||
let index = this.recurringInvoices.findIndex(
|
||||
(_inv) => _inv.id === invoice.id
|
||||
)
|
||||
this.recurringInvoices.splice(index, 1)
|
||||
})
|
||||
this.selectedRecurringInvoices = []
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
resetSelectedCustomer() {
|
||||
this.newRecurringInvoice.customer = null
|
||||
this.newRecurringInvoice.customer_id = ''
|
||||
},
|
||||
|
||||
selectRecurringInvoice(data) {
|
||||
this.selectedRecurringInvoices = data
|
||||
if (
|
||||
this.selectedRecurringInvoices.length ===
|
||||
this.recurringInvoices.length
|
||||
) {
|
||||
this.selectAllField = true
|
||||
} else {
|
||||
this.selectAllField = false
|
||||
}
|
||||
},
|
||||
|
||||
selectAllRecurringInvoices() {
|
||||
if (
|
||||
this.selectedRecurringInvoices.length ===
|
||||
this.recurringInvoices.length
|
||||
) {
|
||||
this.selectedRecurringInvoices = []
|
||||
this.selectAllField = false
|
||||
} else {
|
||||
let allInvoiceIds = this.recurringInvoices.map(
|
||||
(invoice) => invoice.id
|
||||
)
|
||||
this.selectedRecurringInvoices = allInvoiceIds
|
||||
this.selectAllField = true
|
||||
}
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.newRecurringInvoice.items.push({
|
||||
...recurringInvoiceItemStub,
|
||||
id: Guid.raw(),
|
||||
taxes: [{ ...TaxStub, id: Guid.raw() }],
|
||||
})
|
||||
},
|
||||
|
||||
removeItem(index) {
|
||||
this.newRecurringInvoice.items.splice(index, 1)
|
||||
},
|
||||
|
||||
updateItem(data) {
|
||||
Object.assign(this.newRecurringInvoice.items[data.index], { ...data })
|
||||
},
|
||||
|
||||
async fetchRecurringInvoiceInitialSettings(isEdit) {
|
||||
const companyStore = useCompanyStore()
|
||||
const customerStore = useCustomerStore()
|
||||
const itemStore = useItemStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const route = useRoute()
|
||||
|
||||
this.isFetchingInitialSettings = true
|
||||
this.newRecurringInvoice.currency = companyStore.selectedCompanyCurrency
|
||||
|
||||
if (route.query.customer) {
|
||||
let response = await customerStore.fetchCustomer(route.query.customer)
|
||||
this.newRecurringInvoice.customer = response.data.data
|
||||
this.selectCustomer(response.data.data.id)
|
||||
}
|
||||
|
||||
let editActions = []
|
||||
|
||||
// on create
|
||||
if (!isEdit) {
|
||||
this.newRecurringInvoice.tax_per_item =
|
||||
companyStore.selectedCompanySettings.tax_per_item
|
||||
this.newRecurringInvoice.discount_per_item =
|
||||
companyStore.selectedCompanySettings.discount_per_item
|
||||
this.newRecurringInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newRecurringInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newRecurringInvoice.starts_at = moment().format('YYYY-MM-DD')
|
||||
this.newRecurringInvoice.next_invoice_date = moment()
|
||||
.add(7, 'days')
|
||||
.format('YYYY-MM-DD')
|
||||
} else {
|
||||
editActions = [this.fetchRecurringInvoice(route.params.id)]
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
itemStore.fetchItems({
|
||||
filter: {},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
}),
|
||||
this.resetSelectedNote(),
|
||||
invoiceStore.fetchInvoiceTemplates(),
|
||||
taxTypeStore.fetchTaxTypes({ limit: 'all' }),
|
||||
...editActions,
|
||||
])
|
||||
.then(async ([res1, res2, res3, res4, res5]) => {
|
||||
if (res3.data) {
|
||||
this.templates = invoiceStore.templates
|
||||
}
|
||||
|
||||
if (!isEdit) {
|
||||
this.setTemplate(this.templates[0].name)
|
||||
}
|
||||
|
||||
if (isEdit && res5?.data) {
|
||||
let data = {
|
||||
...res5.data.data,
|
||||
}
|
||||
|
||||
this.setTemplate(res5?.data?.data?.template_name)
|
||||
}
|
||||
if (isEdit) {
|
||||
this.addSalesTaxUs()
|
||||
}
|
||||
this.isFetchingInitialSettings = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
handleError(err)
|
||||
})
|
||||
},
|
||||
|
||||
addSalesTaxUs() {
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
let salesTax = { ...TaxStub }
|
||||
let found = this.newRecurringInvoice.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)
|
||||
}
|
||||
},
|
||||
|
||||
setTemplate(data) {
|
||||
this.newRecurringInvoice.template_name = data
|
||||
},
|
||||
|
||||
setSelectedFrequency() {
|
||||
let data = this.frequencies.find(
|
||||
(frequency) => {
|
||||
return frequency.value === this.newRecurringInvoice.frequency
|
||||
}
|
||||
)
|
||||
data ? this.newRecurringInvoice.selectedFrequency = data
|
||||
: this.newRecurringInvoice.selectedFrequency = { label: 'Custom', value: 'CUSTOM' }
|
||||
|
||||
},
|
||||
|
||||
resetSelectedNote() {
|
||||
this.newRecurringInvoice.selectedNote = null
|
||||
},
|
||||
|
||||
fetchRecurringInvoiceFrequencyDate(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get('/api/v1/recurring-invoice-frequency', { params })
|
||||
.then((response) => {
|
||||
this.newRecurringInvoice.next_invoice_at =
|
||||
response.data.next_invoice_at
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: global.t('errors.enter_valid_cron_format'),
|
||||
})
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
91
resources/scripts/admin/stores/reset.js
Normal file
91
resources/scripts/admin/stores/reset.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useBackupStore } from './backup'
|
||||
import { useCategoryStore } from './category'
|
||||
import { useCompanyStore } from './company'
|
||||
import { useCustomFieldStore } from './custom-field'
|
||||
import { useCustomerStore } from './customer'
|
||||
import { useDashboardStore } from './dashboard'
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useDiskStore } from './disk'
|
||||
import { useEstimateStore } from './estimate'
|
||||
import { useExchangeRateStore } from './exchange-rate'
|
||||
import { useExpenseStore } from './expense'
|
||||
import { useGlobalStore } from './global'
|
||||
import { useInstallationStore } from './installation'
|
||||
import { useInvoiceStore } from './invoice'
|
||||
import { useItemStore } from './item'
|
||||
import { useMailDriverStore } from './mail-driver'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useNotesStore } from './note'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { usePaymentStore } from './payment'
|
||||
import { useRecurringInvoiceStore } from './recurring-invoice'
|
||||
import { useRoleStore } from './role'
|
||||
import { useTaxTypeStore } from './tax-type'
|
||||
import { useUserStore } from './user'
|
||||
import { useUsersStore } from './users'
|
||||
|
||||
export const useResetStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'reset',
|
||||
actions: {
|
||||
clearPinia() {
|
||||
const backupStore = useBackupStore()
|
||||
const categoryStore = useCategoryStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const customFieldStore = useCustomFieldStore()
|
||||
const customerStore = useCustomerStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const diskStore = useDiskStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const expenseStore = useExpenseStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const installationStore = useInstallationStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const itemStore = useItemStore()
|
||||
const mailDriverStore = useMailDriverStore()
|
||||
const modalStore = useModalStore()
|
||||
const noteStore = useNotesStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
const roleStore = useRoleStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const userStore = useUserStore()
|
||||
const usersStore = useUsersStore()
|
||||
|
||||
backupStore.$reset()
|
||||
categoryStore.$reset()
|
||||
companyStore.$reset()
|
||||
customFieldStore.$reset()
|
||||
customerStore.$reset()
|
||||
dashboardStore.$reset()
|
||||
dialogStore.$reset()
|
||||
diskStore.$reset()
|
||||
estimateStore.$reset()
|
||||
exchangeRateStore.$reset()
|
||||
expenseStore.$reset()
|
||||
globalStore.$reset()
|
||||
installationStore.$reset()
|
||||
invoiceStore.$reset()
|
||||
itemStore.$reset()
|
||||
mailDriverStore.$reset()
|
||||
modalStore.$reset()
|
||||
noteStore.$reset()
|
||||
notificationStore.$reset()
|
||||
paymentStore.$reset()
|
||||
recurringInvoiceStore.$reset()
|
||||
roleStore.$reset()
|
||||
taxTypeStore.$reset()
|
||||
userStore.$reset()
|
||||
usersStore.$reset()
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
169
resources/scripts/admin/stores/role.js
Normal file
169
resources/scripts/admin/stores/role.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import _ from 'lodash'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const useRoleStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'role',
|
||||
state: () => ({
|
||||
roles: [],
|
||||
allAbilities: [],
|
||||
selectedRoles: [],
|
||||
currentRole: {
|
||||
id: null,
|
||||
name: '',
|
||||
abilities: [],
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isEdit: (state) => (state.currentRole.id ? true : false),
|
||||
abilitiesList: (state) => {
|
||||
let abilities = state.allAbilities.map((a) => ({
|
||||
modelName: a.model
|
||||
? a.model.substring(a.model.lastIndexOf('\\') + 1)
|
||||
: 'Common',
|
||||
disabled: false,
|
||||
...a,
|
||||
}))
|
||||
return _.groupBy(abilities, 'modelName')
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchRoles(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/roles`, { params })
|
||||
.then((response) => {
|
||||
this.roles = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchRole(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/roles/${id}`)
|
||||
.then((response) => {
|
||||
this.currentRole.name = response.data.data.name
|
||||
this.currentRole.id = response.data.data.id
|
||||
|
||||
response.data.data.abilities.forEach((_ra) => {
|
||||
for (const property in this.abilitiesList) {
|
||||
this.abilitiesList[property].forEach((_p) => {
|
||||
if (_p.ability === _ra.name) {
|
||||
this.currentRole.abilities.push(_p)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addRole(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/roles', data)
|
||||
.then((response) => {
|
||||
this.roles.push(response.data.role)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.roles.created_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateRole(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/roles/${data.id}`, data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.roles.findIndex(
|
||||
(role) => role.id === response.data.data.id
|
||||
)
|
||||
this.roles[pos] = data.role
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.roles.updated_message'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchAbilities(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.allAbilities.length) {
|
||||
resolve(this.allAbilities)
|
||||
} else {
|
||||
axios
|
||||
.get(`/api/v1/abilities`, { params })
|
||||
.then((response) => {
|
||||
this.allAbilities = response.data.abilities
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
deleteRole(id) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.delete(`/api/v1/roles/${id}`)
|
||||
.then((response) => {
|
||||
let index = this.roles.findIndex((role) => role.id === id)
|
||||
this.roles.splice(index, 1)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.roles.deleted_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
164
resources/scripts/admin/stores/tax-type.js
Normal file
164
resources/scripts/admin/stores/tax-type.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const useTaxTypeStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'taxType',
|
||||
|
||||
state: () => ({
|
||||
taxTypes: [],
|
||||
currentTaxType: {
|
||||
id: null,
|
||||
name: '',
|
||||
percent: 0,
|
||||
description: '',
|
||||
compound_tax: false,
|
||||
collective_tax: 0,
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isEdit: (state) => (state.currentTaxType.id ? true : false),
|
||||
},
|
||||
|
||||
actions: {
|
||||
resetCurrentTaxType() {
|
||||
this.currentTaxType = {
|
||||
id: null,
|
||||
name: '',
|
||||
percent: 0,
|
||||
description: '',
|
||||
compound_tax: false,
|
||||
collective_tax: 0,
|
||||
}
|
||||
},
|
||||
|
||||
fetchTaxTypes(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/tax-types`, { params })
|
||||
.then((response) => {
|
||||
this.taxTypes = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchTaxType(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/tax-types/${id}`)
|
||||
.then((response) => {
|
||||
this.currentTaxType = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addTaxType(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/tax-types', data)
|
||||
.then((response) => {
|
||||
this.taxTypes.push(response.data.data)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.tax_types.created_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateTaxType(data) {
|
||||
const notificationStore = useNotificationStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/tax-types/${data.id}`, data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.taxTypes.findIndex(
|
||||
(taxTypes) => taxTypes.id === response.data.data.id
|
||||
)
|
||||
this.taxTypes[pos] = data.taxTypes
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.tax_types.updated_message'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchSalesTax(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/m/sales-tax-us/current-tax', data)
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
let pos = this.taxTypes.findIndex(
|
||||
(_t) => _t.name === 'SalesTaxUs'
|
||||
)
|
||||
pos > -1 ? this.taxTypes.splice(pos, 1) : ''
|
||||
this.taxTypes.push({ ...response.data.data, tax_type_id: response.data.data.id })
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteTaxType(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.delete(`/api/v1/tax-types/${id}`)
|
||||
.then((response) => {
|
||||
if (response.data.success) {
|
||||
let index = this.taxTypes.findIndex(
|
||||
(taxType) => taxType.id === id
|
||||
)
|
||||
this.taxTypes.splice(index, 1)
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.tax_types.deleted_message'),
|
||||
})
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
147
resources/scripts/admin/stores/user.js
Normal file
147
resources/scripts/admin/stores/user.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const useUserStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'user',
|
||||
|
||||
state: () => ({
|
||||
currentUser: null,
|
||||
currentAbilities: [],
|
||||
currentUserSettings: {},
|
||||
|
||||
userForm: {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
language: '',
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
currentAbilitiesCount: (state) => state.currentAbilities.length,
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateCurrentUser(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put('/api/v1/me', data)
|
||||
.then((response) => {
|
||||
this.currentUser = response.data.data
|
||||
Object.assign(this.userForm, response.data.data)
|
||||
const notificationStore = useNotificationStore()
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('settings.account_settings.updated_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchCurrentUser(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/me`, params)
|
||||
.then((response) => {
|
||||
this.currentUser = response.data.data
|
||||
this.userForm = response.data.data
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
uploadAvatar(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/api/v1/me/upload-avatar', data)
|
||||
.then((response) => {
|
||||
this.currentUser.avatar = response.data.data.avatar
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
fetchUserSettings(settings) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get('/api/v1/me/settings', {
|
||||
params: {
|
||||
settings,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateUserSettings(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put('/api/v1/me/settings', data)
|
||||
.then((response) => {
|
||||
if (data.settings.language) {
|
||||
this.currentUserSettings.language = data.settings.language
|
||||
global.locale = data.settings.language
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
hasAbilities(abilities) {
|
||||
return !!this.currentAbilities.find((ab) => {
|
||||
if (ab.name === '*') return true
|
||||
if (typeof abilities === 'string') {
|
||||
return ab.name === abilities
|
||||
}
|
||||
return !!abilities.find((p) => {
|
||||
return ab.name === p
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
hasAllAbilities(abilities) {
|
||||
let isAvailable = true
|
||||
this.currentAbilities.filter((ab) => {
|
||||
let hasContain = !!abilities.find((p) => {
|
||||
return ab.name === p
|
||||
})
|
||||
if (!hasContain) {
|
||||
isAvailable = false
|
||||
}
|
||||
})
|
||||
|
||||
return isAvailable
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
232
resources/scripts/admin/stores/users.js
Normal file
232
resources/scripts/admin/stores/users.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { handleError } from '@/scripts/helpers/error-handling'
|
||||
|
||||
export const useUsersStore = (useWindow = false) => {
|
||||
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
|
||||
const { global } = window.i18n
|
||||
|
||||
return defineStoreFunc({
|
||||
id: 'users',
|
||||
state: () => ({
|
||||
roles: [],
|
||||
users: [],
|
||||
totalUsers: 0,
|
||||
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) => {
|
||||
axios
|
||||
.get(`/api/v1/users`, { 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) => {
|
||||
axios
|
||||
.get(`/api/v1/users/${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) => {
|
||||
axios
|
||||
.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) => {
|
||||
axios
|
||||
.post('/api/v1/users', data)
|
||||
.then((response) => {
|
||||
this.users.push(response.data)
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('users.created_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
updateUser(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.put(`/api/v1/users/${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('users.updated_message'),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteUser(id) {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/users/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.tc('users.deleted_message', 1),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
deleteMultipleUsers() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(`/api/v1/users/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.tc('users.deleted_message', 2),
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
searchUsers(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.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
|
||||
}
|
||||
},
|
||||
},
|
||||
})()
|
||||
}
|
||||
79
resources/scripts/admin/stub/abilities.js
Normal file
79
resources/scripts/admin/stub/abilities.js
Normal file
@@ -0,0 +1,79 @@
|
||||
export default {
|
||||
DASHBOARD: 'dashboard',
|
||||
|
||||
// customers
|
||||
CREATE_CUSTOMER: 'create-customer',
|
||||
DELETE_CUSTOMER: 'delete-customer',
|
||||
EDIT_CUSTOMER: 'edit-customer',
|
||||
VIEW_CUSTOMER: 'view-customer',
|
||||
|
||||
// Items
|
||||
CREATE_ITEM: 'create-item',
|
||||
DELETE_ITEM: 'delete-item',
|
||||
EDIT_ITEM: 'edit-item',
|
||||
VIEW_ITEM: 'view-item',
|
||||
|
||||
// Tax Types
|
||||
CREATE_TAX_TYPE: 'create-tax-type',
|
||||
DELETE_TAX_TYPE: 'delete-tax-type',
|
||||
EDIT_TAX_TYPE: 'edit-tax-type',
|
||||
VIEW_TAX_TYPE: 'view-tax-type',
|
||||
|
||||
// Estimates
|
||||
CREATE_ESTIMATE: 'create-estimate',
|
||||
DELETE_ESTIMATE: 'delete-estimate',
|
||||
EDIT_ESTIMATE: 'edit-estimate',
|
||||
VIEW_ESTIMATE: 'view-estimate',
|
||||
SEND_ESTIMATE: 'send-estimate',
|
||||
|
||||
// Invoices
|
||||
CREATE_INVOICE: 'create-invoice',
|
||||
DELETE_INVOICE: 'delete-invoice',
|
||||
EDIT_INVOICE: 'edit-invoice',
|
||||
VIEW_INVOICE: 'view-invoice',
|
||||
SEND_INVOICE: 'send-invoice',
|
||||
|
||||
// Recurring Invoices
|
||||
CREATE_RECURRING_INVOICE: 'create-recurring-invoice',
|
||||
DELETE_RECURRING_INVOICE: 'delete-recurring-invoice',
|
||||
EDIT_RECURRING_INVOICE: 'edit-recurring-invoice',
|
||||
VIEW_RECURRING_INVOICE: 'view-recurring-invoice',
|
||||
|
||||
// Payment
|
||||
CREATE_PAYMENT: 'create-payment',
|
||||
DELETE_PAYMENT: 'delete-payment',
|
||||
EDIT_PAYMENT: 'edit-payment',
|
||||
VIEW_PAYMENT: 'view-payment',
|
||||
SEND_PAYMENT: 'send-payment',
|
||||
|
||||
// Payment
|
||||
CREATE_EXPENSE: 'create-expense',
|
||||
DELETE_EXPENSE: 'delete-expense',
|
||||
EDIT_EXPENSE: 'edit-expense',
|
||||
VIEW_EXPENSE: 'view-expense',
|
||||
|
||||
// Custom fields
|
||||
CREATE_CUSTOM_FIELDS: 'create-custom-field',
|
||||
DELETE_CUSTOM_FIELDS: 'delete-custom-field',
|
||||
EDIT_CUSTOM_FIELDS: 'edit-custom-field',
|
||||
VIEW_CUSTOM_FIELDS: 'view-custom-field',
|
||||
|
||||
// Roles
|
||||
CREATE_ROLE: 'create-role',
|
||||
DELETE_ROLE: 'delete-role',
|
||||
EDIT_ROLE: 'edit-role',
|
||||
VIEW_ROLE: 'view-role',
|
||||
|
||||
// exchange rates
|
||||
VIEW_EXCHANGE_RATE: 'view-exchange-rate-provider',
|
||||
CREATE_EXCHANGE_RATE: 'create-exchange-rate-provider',
|
||||
EDIT_EXCHANGE_RATE: 'edit-exchange-rate-provider',
|
||||
DELETE_EXCHANGE_RATE: 'delete-exchange-rate-provider',
|
||||
|
||||
// Reports
|
||||
VIEW_FINANCIAL_REPORT: 'view-financial-reports',
|
||||
|
||||
// settings
|
||||
MANAGE_NOTE: 'manage-all-notes',
|
||||
VIEW_NOTE: 'view-all-notes',
|
||||
}
|
||||
11
resources/scripts/admin/stub/address.js
Normal file
11
resources/scripts/admin/stub/address.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: null,
|
||||
phone: null,
|
||||
address_street_1: null,
|
||||
address_street_2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
country_id: null,
|
||||
zip: null,
|
||||
type: null,
|
||||
}
|
||||
12
resources/scripts/admin/stub/custom-field.js
Normal file
12
resources/scripts/admin/stub/custom-field.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
id: null,
|
||||
label: null,
|
||||
type: null,
|
||||
name: null,
|
||||
default_answer: null,
|
||||
is_required: false,
|
||||
placeholder: null,
|
||||
model_type: null,
|
||||
order: 1,
|
||||
options: [],
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user