mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 19:51:09 -04:00
v5.0.0 update
This commit is contained in:
157
resources/scripts/helpers/error-handling.js
Normal file
157
resources/scripts/helpers/error-handling.js
Normal file
@ -0,0 +1,157 @@
|
||||
import { useAuthStore } from '@/scripts/stores/auth'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
|
||||
export const handleError = (err) => {
|
||||
const authStore = useAuthStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
if (!err.response) {
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Please check your internet connection or wait until servers are back online.',
|
||||
})
|
||||
} else {
|
||||
if (
|
||||
err.response.data &&
|
||||
(err.response.statusText === 'Unauthorized' ||
|
||||
err.response.data === ' Unauthorized.')
|
||||
) {
|
||||
// Unauthorized and log out
|
||||
const msg = err.response.data.message
|
||||
? err.response.data.message
|
||||
: 'Unauthorized'
|
||||
|
||||
showToaster(msg)
|
||||
|
||||
authStore.logout()
|
||||
} else if (err.response.data.errors) {
|
||||
// Show a notification per error
|
||||
const errors = JSON.parse(JSON.stringify(err.response.data.errors))
|
||||
for (const i in errors) {
|
||||
showError(errors[i][0])
|
||||
}
|
||||
} else if (err.response.data.error) {
|
||||
showError(err.response.data.error)
|
||||
} else {
|
||||
showError(err.response.data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const showError = (error) => {
|
||||
switch (error) {
|
||||
case 'These credentials do not match our records.':
|
||||
showToaster('errors.login_invalid_credentials')
|
||||
break
|
||||
case 'invalid_key':
|
||||
showToaster('errors.invalid_provider_key')
|
||||
break
|
||||
|
||||
case 'This feature is available on Starter plan and onwards!':
|
||||
showToaster('errors.starter_plan')
|
||||
break
|
||||
|
||||
case 'taxes_attached':
|
||||
showToaster('settings.tax_types.already_in_use')
|
||||
break
|
||||
|
||||
case 'expense_attached':
|
||||
showToaster('settings.expense_category.already_in_use')
|
||||
break
|
||||
|
||||
case 'payments_attached':
|
||||
showToaster('settings.payment_modes.already_in_use')
|
||||
break
|
||||
|
||||
case 'role_attached_to_users':
|
||||
showToaster('settings.roles.already_in_use')
|
||||
break
|
||||
|
||||
case 'items_attached':
|
||||
showToaster('settings.customization.items.already_in_use')
|
||||
break
|
||||
|
||||
case 'payment_attached_message':
|
||||
showToaster('invoices.payment_attached_message')
|
||||
break
|
||||
|
||||
case 'The email has already been taken.':
|
||||
showToaster('validation.email_already_taken')
|
||||
break
|
||||
|
||||
case 'Relation estimateItems exists.':
|
||||
showToaster('items.item_attached_message')
|
||||
break
|
||||
|
||||
case 'Relation invoiceItems exists.':
|
||||
showToaster('items.item_attached_message')
|
||||
break
|
||||
|
||||
case 'Relation taxes exists.':
|
||||
showToaster('settings.tax_types.already_in_use')
|
||||
break
|
||||
|
||||
case 'Relation taxes exists.':
|
||||
showToaster('settings.tax_types.already_in_use')
|
||||
break
|
||||
|
||||
case 'Relation payments exists.':
|
||||
showToaster('errors.payment_attached')
|
||||
break
|
||||
|
||||
case 'The estimate number has already been taken.':
|
||||
showToaster('errors.estimate_number_used')
|
||||
break
|
||||
|
||||
case 'The payment number has already been taken.':
|
||||
showToaster('errors.estimate_number_used')
|
||||
break
|
||||
|
||||
case 'The invoice number has already been taken.':
|
||||
showToaster('errors.invoice_number_used')
|
||||
break
|
||||
|
||||
case 'The name has already been taken.':
|
||||
showToaster('errors.name_already_taken')
|
||||
break
|
||||
|
||||
case 'total_invoice_amount_must_be_more_than_paid_amount':
|
||||
showToaster('invoices.invalid_due_amount_message')
|
||||
break
|
||||
|
||||
case 'you_cannot_edit_currency':
|
||||
showToaster('customers.edit_currency_not_allowed')
|
||||
break
|
||||
|
||||
case 'receipt_does_not_exist':
|
||||
showToaster('errors.receipt_does_not_exist')
|
||||
break
|
||||
|
||||
case 'customer_cannot_be_changed_after_payment_is_added':
|
||||
showToaster('errors.customer_cannot_be_changed_after_payment_is_added')
|
||||
break
|
||||
|
||||
case 'invalid_credentials':
|
||||
showToaster('errors.invalid_credentials')
|
||||
break
|
||||
|
||||
case 'not_allowed':
|
||||
showToaster('errors.not_allowed')
|
||||
break
|
||||
|
||||
default:
|
||||
showToaster(error, false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export const showToaster = (msg, t = true) => {
|
||||
const { global } = window.i18n
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'error',
|
||||
message: t ? global.t(msg) : msg,
|
||||
})
|
||||
}
|
||||
27
resources/scripts/helpers/use-popper.js
Normal file
27
resources/scripts/helpers/use-popper.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { ref, onMounted, watchEffect } from 'vue'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
export function usePopper(options) {
|
||||
let activator = ref(null)
|
||||
let container = ref(null)
|
||||
let popper = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect(onInvalidate => {
|
||||
if (!container.value) return
|
||||
if (!activator.value) return
|
||||
|
||||
let containerEl = container.value.el || container.value
|
||||
let activatorEl = activator.value.el || activator.value
|
||||
|
||||
if (!(activatorEl instanceof HTMLElement)) return
|
||||
if (!(containerEl instanceof HTMLElement)) return
|
||||
|
||||
popper.value = createPopper(activatorEl, containerEl, options)
|
||||
|
||||
onInvalidate(popper.value.destroy)
|
||||
})
|
||||
})
|
||||
|
||||
return [activator, container, popper]
|
||||
}
|
||||
280
resources/scripts/helpers/utilities.js
Normal file
280
resources/scripts/helpers/utilities.js
Normal file
@ -0,0 +1,280 @@
|
||||
import i18n from '../plugins/i18n'
|
||||
const { global } = i18n
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { isArray } from 'lodash'
|
||||
|
||||
export default {
|
||||
isImageFile(fileType) {
|
||||
const validImageTypes = ['image/gif', 'image/jpeg', 'image/png']
|
||||
|
||||
return validImageTypes.includes(fileType)
|
||||
},
|
||||
addClass(el, className) {
|
||||
if (el.classList) el.classList.add(className)
|
||||
else el.className += ' ' + className
|
||||
},
|
||||
hasClass(el, className) {
|
||||
const hasClass = el.classList
|
||||
? el.classList.contains(className)
|
||||
: new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className)
|
||||
|
||||
return hasClass
|
||||
},
|
||||
|
||||
formatMoney(amount, currency = 0) {
|
||||
if (!currency) {
|
||||
currency = {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
}
|
||||
}
|
||||
|
||||
amount = amount / 100
|
||||
|
||||
let {
|
||||
precision,
|
||||
decimal_separator,
|
||||
thousand_separator,
|
||||
symbol,
|
||||
swap_currency_symbol,
|
||||
} = currency
|
||||
|
||||
try {
|
||||
precision = Math.abs(precision)
|
||||
precision = isNaN(precision) ? 2 : precision
|
||||
|
||||
const negativeSign = amount < 0 ? '-' : ''
|
||||
|
||||
let i = parseInt(
|
||||
(amount = Math.abs(Number(amount) || 0).toFixed(precision))
|
||||
).toString()
|
||||
let j = i.length > 3 ? i.length % 3 : 0
|
||||
|
||||
let moneySymbol = `${symbol}`
|
||||
let thousandText = j ? i.substr(0, j) + thousand_separator : ''
|
||||
let amountText = i
|
||||
.substr(j)
|
||||
.replace(/(\d{3})(?=\d)/g, '$1' + thousand_separator)
|
||||
let precisionText = precision
|
||||
? decimal_separator +
|
||||
Math.abs(amount - i)
|
||||
.toFixed(precision)
|
||||
.slice(2)
|
||||
: ''
|
||||
let combinedAmountText =
|
||||
negativeSign + thousandText + amountText + precisionText
|
||||
|
||||
return swap_currency_symbol
|
||||
? combinedAmountText + ' ' + moneySymbol
|
||||
: moneySymbol + ' ' + combinedAmountText
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
// Merge two objects but only existing properties
|
||||
mergeSettings(target, source) {
|
||||
Object.keys(source).forEach(function (key) {
|
||||
if (key in target) {
|
||||
// or target.hasOwnProperty(key)
|
||||
target[key] = source[key]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
checkValidUrl(url) {
|
||||
if (
|
||||
url.includes('http://localhost') ||
|
||||
url.includes('http://127.0.0.1') ||
|
||||
url.includes('https://localhost') ||
|
||||
url.includes('https://127.0.0.1')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
let pattern = new RegExp(
|
||||
'^(https?:\\/\\/)?' + // protocol
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
|
||||
'(\\#[-a-z\\d_]*)?$',
|
||||
'i'
|
||||
) // fragment locator
|
||||
|
||||
return !!pattern.test(url)
|
||||
},
|
||||
|
||||
checkValidDomainUrl(url) {
|
||||
if (url.includes('localhost')) {
|
||||
return true
|
||||
}
|
||||
let pattern = new RegExp(
|
||||
'^([0-9A-Za-z-\\.@:%_+~#=]+)+((\\.[a-zA-Z]{2,3})+)(/(.)*)?(\\?(.)*)?'
|
||||
)
|
||||
return !!pattern.test(url)
|
||||
},
|
||||
|
||||
fallbackCopyTextToClipboard(text) {
|
||||
var textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0'
|
||||
textArea.style.left = '0'
|
||||
textArea.style.position = 'fixed'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
try {
|
||||
var successful = document.execCommand('copy')
|
||||
var msg = successful ? 'successful' : 'unsuccessful'
|
||||
console.log('Fallback: Copying text command was ' + msg)
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err)
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
},
|
||||
copyTextToClipboard(text) {
|
||||
if (!navigator.clipboard) {
|
||||
this.fallbackCopyTextToClipboard(text)
|
||||
return
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
return true
|
||||
},
|
||||
function (err) {
|
||||
return false
|
||||
}
|
||||
)
|
||||
},
|
||||
arrayDifference(array1, array2) {
|
||||
return array1?.filter((i) => {
|
||||
return array2?.indexOf(i) < 0
|
||||
})
|
||||
},
|
||||
getBadgeStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'DRAFT':
|
||||
return {
|
||||
bgColor: '#F8EDCB',
|
||||
color: '#744210',
|
||||
}
|
||||
case 'PAID':
|
||||
return {
|
||||
bgColor: '#D5EED0',
|
||||
color: '#276749',
|
||||
}
|
||||
case 'UNPAID':
|
||||
return {
|
||||
bgColor: '#F8EDC',
|
||||
color: '#744210',
|
||||
}
|
||||
case 'SENT':
|
||||
return {
|
||||
bgColor: 'rgba(246, 208, 154, 0.4)',
|
||||
color: '#975a16',
|
||||
}
|
||||
case 'REJECTED':
|
||||
return {
|
||||
bgColor: '#E1E0EA',
|
||||
color: '#1A1841',
|
||||
}
|
||||
case 'ACCEPTED':
|
||||
return {
|
||||
bgColor: '#D5EED0',
|
||||
color: '#276749',
|
||||
}
|
||||
case 'VIEWED':
|
||||
return {
|
||||
bgColor: '#C9E3EC',
|
||||
color: '#2c5282',
|
||||
}
|
||||
case 'EXPIRED':
|
||||
return {
|
||||
bgColor: '#FED7D7',
|
||||
color: '#c53030',
|
||||
}
|
||||
case 'PARTIALLY PAID':
|
||||
return {
|
||||
bgColor: '#C9E3EC',
|
||||
color: '#2c5282',
|
||||
}
|
||||
case 'OVERDUE':
|
||||
return {
|
||||
bgColor: '#FED7D7',
|
||||
color: '#c53030',
|
||||
}
|
||||
case 'COMPLETED':
|
||||
return {
|
||||
bgColor: '#D5EED0',
|
||||
color: '#276749',
|
||||
}
|
||||
case 'DUE':
|
||||
return {
|
||||
bgColor: '#F8EDCB',
|
||||
color: '#744210',
|
||||
}
|
||||
case 'YES':
|
||||
return {
|
||||
bgColor: '#D5EED0',
|
||||
color: '#276749',
|
||||
}
|
||||
case 'NO':
|
||||
return {
|
||||
bgColor: '#FED7D7',
|
||||
color: '#c53030',
|
||||
}
|
||||
}
|
||||
},
|
||||
getStatusTranslation(status) {
|
||||
switch (status) {
|
||||
case 'DRAFT':
|
||||
return global.t('general.draft')
|
||||
case 'PAID':
|
||||
return global.t('invoices.paid')
|
||||
case 'UNPAID':
|
||||
return global.t('invoices.unpaid')
|
||||
case 'SENT':
|
||||
return global.t('general.sent')
|
||||
case 'REJECTED':
|
||||
return global.t('estimates.rejected')
|
||||
case 'ACCEPTED':
|
||||
return global.t('estimates.accepted')
|
||||
case 'VIEWED':
|
||||
return global.t('invoices.viewed')
|
||||
case 'EXPIRED':
|
||||
return global.t('estimates.expired')
|
||||
case 'PARTIALLY PAID':
|
||||
return global.t('estimates.partially_paid')
|
||||
case 'OVERDUE':
|
||||
return global.t('invoices.overdue')
|
||||
case 'COMPLETED':
|
||||
return global.t('invoices.completed')
|
||||
case 'DUE':
|
||||
return global.t('general.due')
|
||||
default:
|
||||
return status
|
||||
}
|
||||
},
|
||||
toFormData(object) {
|
||||
const formData = new FormData()
|
||||
|
||||
Object.keys(object).forEach((key) => {
|
||||
if (isArray(object[key])) {
|
||||
formData.append(key, JSON.stringify(object[key]))
|
||||
} else {
|
||||
// Convert null to empty strings (because formData does not support null values and converts it to string)
|
||||
if (object[key] === null) {
|
||||
object[key] = ''
|
||||
}
|
||||
|
||||
formData.append(key, object[key])
|
||||
}
|
||||
})
|
||||
|
||||
return formData
|
||||
},
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user