mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 19:51:09 -04:00
v6 update
This commit is contained in:
@ -85,7 +85,7 @@ const variantClass = computed(() => {
|
||||
props.variant === 'primary',
|
||||
'border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200 focus:ring-primary-500':
|
||||
props.variant === 'secondary',
|
||||
'border-transparent border-solid border-primary-500 font-normal transition ease-in-out duration-150 text-primary-500 hover:bg-primary-200 shadow-inner ':
|
||||
'border-transparent border-solid border-primary-500 font-normal transition ease-in-out duration-150 text-primary-500 hover:bg-primary-200 shadow-inner focus:ring-primary-500':
|
||||
props.variant == 'primary-outline',
|
||||
'border-gray-200 text-gray-700 bg-white hover:bg-gray-50 focus:ring-primary-500 focus:ring-offset-0':
|
||||
props.variant == 'white',
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useCustomFieldStore } from '@/scripts/stores/custom-field'
|
||||
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
|
||||
|
||||
const props = defineProps({
|
||||
contentLoading: {
|
||||
|
||||
@ -34,13 +34,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCustomerStore } from '@/scripts/stores/customer'
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import CustomerModal from '@/scripts/components/modal-components/CustomerModal.vue'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import abilities from '@/scripts/stub/abilities'
|
||||
import CustomerModal from '@/scripts/admin/components/modal-components/CustomerModal.vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
<div v-else class="max-h-[173px]">
|
||||
<CustomerModal />
|
||||
<!-- <SalesTax :type="type" /> -->
|
||||
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
@ -198,8 +199,8 @@
|
||||
class="
|
||||
flex
|
||||
justify-center
|
||||
w-10
|
||||
h-10
|
||||
!w-10
|
||||
!h-10
|
||||
p-2
|
||||
mr-5
|
||||
text-sm text-white
|
||||
@ -384,19 +385,19 @@
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/stores/recurring-invoice'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
import { useCustomerStore } from '@/scripts/stores/customer'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
import { useCustomerStore } from '@/scripts/admin/stores/customer'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import abilities from '@/scripts/stub/abilities'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
import { useRoute } from 'vue-router'
|
||||
import CustomerModal from '@/scripts/components/modal-components/CustomerModal.vue'
|
||||
import CustomerModal from '@/scripts/admin/components/modal-components/CustomerModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
valid: {
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
import FlatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import { computed, reactive, watch, ref, useSlots } from 'vue'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
const dp = ref(null)
|
||||
|
||||
|
||||
@ -62,11 +62,11 @@
|
||||
text-left
|
||||
align-bottom
|
||||
transition-all
|
||||
transform
|
||||
bg-white
|
||||
rounded-lg
|
||||
shadow-xl
|
||||
sm:my-8 sm:align-middle sm:w-full sm:p-6
|
||||
relative
|
||||
"
|
||||
:class="dialogSizeClasses"
|
||||
>
|
||||
|
||||
@ -18,11 +18,11 @@
|
||||
<div ref="container" class="z-10" :class="widthClass">
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
enter-from-class="scale-95 opacity-0"
|
||||
enter-to-class="scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
leave-from-class="scale-100 opacity-100"
|
||||
leave-to-class="scale-95 opacity-0"
|
||||
>
|
||||
<MenuItems :class="containerClasses">
|
||||
<div class="py-1">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
|
||||
@ -1,193 +1,3 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import utils from '@/scripts/helpers/utilities'
|
||||
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
avatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoProcess: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
preserveLocalFiles: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: 'image/*',
|
||||
},
|
||||
inputFieldName: {
|
||||
type: String,
|
||||
default: 'photos',
|
||||
},
|
||||
base64: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'remove', 'update:modelValue'])
|
||||
|
||||
// status
|
||||
const STATUS_INITIAL = 0
|
||||
const STATUS_SAVING = 1
|
||||
const STATUS_SUCCESS = 2
|
||||
const STATUS_FAILED = 3
|
||||
|
||||
let uploadedFiles = ref([])
|
||||
const localFiles = ref([])
|
||||
const inputRef = ref(null)
|
||||
let uploadError = ref(null)
|
||||
let currentStatus = ref(null)
|
||||
|
||||
function reset() {
|
||||
// reset form to initial state
|
||||
currentStatus = STATUS_INITIAL
|
||||
|
||||
uploadedFiles.value = []
|
||||
|
||||
if (props.modelValue && props.modelValue.length) {
|
||||
localFiles.value = [...props.modelValue]
|
||||
} else {
|
||||
localFiles.value = []
|
||||
}
|
||||
|
||||
uploadError = null
|
||||
}
|
||||
|
||||
function upload(formData) {
|
||||
return (
|
||||
axios
|
||||
.post(props.uploadUrl, formData)
|
||||
// get data
|
||||
.then((x) => x.data)
|
||||
// add url field
|
||||
.then((x) => x.map((img) => ({ ...img, url: `/images/${img.id}` })))
|
||||
)
|
||||
}
|
||||
|
||||
// upload data to the server
|
||||
function save(formData) {
|
||||
currentStatus = STATUS_SAVING
|
||||
|
||||
upload(formData)
|
||||
.then((x) => {
|
||||
uploadedFiles = [].concat(x)
|
||||
currentStatus = STATUS_SUCCESS
|
||||
})
|
||||
.catch((err) => {
|
||||
uploadError = err.response
|
||||
currentStatus = STATUS_FAILED
|
||||
})
|
||||
}
|
||||
|
||||
function getBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
function onChange(fieldName, fileList, fileCount) {
|
||||
if (!fileList.length) return
|
||||
|
||||
if (props.multiple) {
|
||||
emit('change', fieldName, fileList, fileCount)
|
||||
} else {
|
||||
if (props.base64) {
|
||||
getBase64(fileList[0]).then((res) => {
|
||||
emit('change', fieldName, res, fileCount, fileList[0])
|
||||
})
|
||||
} else {
|
||||
emit('change', fieldName, fileList[0], fileCount)
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.preserveLocalFiles) {
|
||||
localFiles.value = []
|
||||
}
|
||||
|
||||
Array.from(Array(fileList.length).keys()).forEach((x) => {
|
||||
const file = fileList[x]
|
||||
|
||||
if (utils.isImageFile(file.type)) {
|
||||
getBase64(file).then((image) => {
|
||||
localFiles.value.push({
|
||||
fileObject: file,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
image,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
localFiles.value.push({
|
||||
fileObject: file,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
emit('update:modelValue', localFiles.value)
|
||||
|
||||
if (!props.autoProcess) return
|
||||
|
||||
// append the files to FormData
|
||||
const formData = new FormData()
|
||||
|
||||
Array.from(Array(fileList.length).keys()).forEach((x) => {
|
||||
formData.append(fieldName, fileList[x], fileList[x].name)
|
||||
})
|
||||
|
||||
// save it
|
||||
save(formData)
|
||||
}
|
||||
|
||||
function onBrowse() {
|
||||
if (inputRef.value) {
|
||||
inputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function onAvatarRemove(image) {
|
||||
localFiles.value = []
|
||||
emit('remove', image)
|
||||
}
|
||||
|
||||
function onFileRemove(index) {
|
||||
localFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reset()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
localFiles.value = [...v]
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
@ -234,7 +44,7 @@ watch(
|
||||
|
||||
<!-- Avatar Not Selected -->
|
||||
<div v-if="!localFiles.length && avatar" class="">
|
||||
<img src="/img/default-avatar.jpg" class="rounded" alt="Default Avatar" />
|
||||
<img :src="getDefaultAvatar()" class="rounded" alt="Default Avatar" />
|
||||
|
||||
<a
|
||||
href="#"
|
||||
@ -277,6 +87,9 @@ watch(
|
||||
</a>
|
||||
to choose a file
|
||||
</p>
|
||||
<p class="text-xs leading-4 text-center text-gray-400 mt-2">
|
||||
{{ recommendedText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -563,3 +376,202 @@ watch(
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import utils from '@/scripts/helpers/utilities'
|
||||
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
avatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoProcess: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
preserveLocalFiles: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: 'image/*',
|
||||
},
|
||||
inputFieldName: {
|
||||
type: String,
|
||||
default: 'photos',
|
||||
},
|
||||
base64: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recommendedText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'remove', 'update:modelValue'])
|
||||
|
||||
// status
|
||||
const STATUS_INITIAL = 0
|
||||
const STATUS_SAVING = 1
|
||||
const STATUS_SUCCESS = 2
|
||||
const STATUS_FAILED = 3
|
||||
|
||||
let uploadedFiles = ref([])
|
||||
const localFiles = ref([])
|
||||
const inputRef = ref(null)
|
||||
let uploadError = ref(null)
|
||||
let currentStatus = ref(null)
|
||||
|
||||
function reset() {
|
||||
// reset form to initial state
|
||||
currentStatus = STATUS_INITIAL
|
||||
|
||||
uploadedFiles.value = []
|
||||
|
||||
if (props.modelValue && props.modelValue.length) {
|
||||
localFiles.value = [...props.modelValue]
|
||||
} else {
|
||||
localFiles.value = []
|
||||
}
|
||||
|
||||
uploadError = null
|
||||
}
|
||||
|
||||
function upload(formData) {
|
||||
return (
|
||||
axios
|
||||
.post(props.uploadUrl, formData)
|
||||
// get data
|
||||
.then((x) => x.data)
|
||||
// add url field
|
||||
.then((x) => x.map((img) => ({ ...img, url: `/images/${img.id}` })))
|
||||
)
|
||||
}
|
||||
|
||||
// upload data to the server
|
||||
function save(formData) {
|
||||
currentStatus = STATUS_SAVING
|
||||
|
||||
upload(formData)
|
||||
.then((x) => {
|
||||
uploadedFiles = [].concat(x)
|
||||
currentStatus = STATUS_SUCCESS
|
||||
})
|
||||
.catch((err) => {
|
||||
uploadError = err.response
|
||||
currentStatus = STATUS_FAILED
|
||||
})
|
||||
}
|
||||
|
||||
function getBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
function onChange(fieldName, fileList, fileCount) {
|
||||
if (!fileList.length) return
|
||||
|
||||
if (props.multiple) {
|
||||
emit('change', fieldName, fileList, fileCount)
|
||||
} else {
|
||||
if (props.base64) {
|
||||
getBase64(fileList[0]).then((res) => {
|
||||
emit('change', fieldName, res, fileCount, fileList[0])
|
||||
})
|
||||
} else {
|
||||
emit('change', fieldName, fileList[0], fileCount)
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.preserveLocalFiles) {
|
||||
localFiles.value = []
|
||||
}
|
||||
|
||||
Array.from(Array(fileList.length).keys()).forEach((x) => {
|
||||
const file = fileList[x]
|
||||
|
||||
if (utils.isImageFile(file.type)) {
|
||||
getBase64(file).then((image) => {
|
||||
localFiles.value.push({
|
||||
fileObject: file,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
image,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
localFiles.value.push({
|
||||
fileObject: file,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
emit('update:modelValue', localFiles.value)
|
||||
|
||||
if (!props.autoProcess) return
|
||||
|
||||
// append the files to FormData
|
||||
const formData = new FormData()
|
||||
|
||||
Array.from(Array(fileList.length).keys()).forEach((x) => {
|
||||
formData.append(fieldName, fileList[x], fileList[x].name)
|
||||
})
|
||||
|
||||
// save it
|
||||
save(formData)
|
||||
}
|
||||
|
||||
function onBrowse() {
|
||||
if (inputRef.value) {
|
||||
inputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function onAvatarRemove(image) {
|
||||
localFiles.value = []
|
||||
emit('remove', image)
|
||||
}
|
||||
|
||||
function onFileRemove(index) {
|
||||
localFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function getDefaultAvatar() {
|
||||
const imgUrl = new URL('/img/default-avatar.jpg', import.meta.url)
|
||||
return imgUrl
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reset()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
localFiles.value = [...v]
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<transition
|
||||
enter-active-class="transition duration-500 ease-in-out"
|
||||
enter-from-class="transform opacity-0"
|
||||
enter-to-class="transform opacity-100"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-from-class="transform opacity-100"
|
||||
leave-to-class="transform opacity-0"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-show="show" class="relative z-10 p-4 md:p-8 bg-gray-200 rounded">
|
||||
<slot name="filter-header" />
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
import { inject, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -34,8 +34,7 @@
|
||||
><i></i><i></i> <i></i><i></i><i></i> <i></i><i></i><i></i> <i></i
|
||||
><i></i><i></i> <i></i><i></i><i></i> <i></i><i></i><i></i>
|
||||
</div>
|
||||
<img
|
||||
src="/img/crater-logo.png"
|
||||
<MainLogo
|
||||
class="
|
||||
absolute
|
||||
block
|
||||
@ -54,14 +53,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
showBgOverlay: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
<script setup>
|
||||
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
|
||||
|
||||
const props = defineProps({
|
||||
showBgOverlay: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
function getCraterLogo() {
|
||||
const imgUrl = new URL('/img/crater-logo.png', import.meta.url)
|
||||
return imgUrl
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -165,7 +169,7 @@ export default {
|
||||
position: absolute;
|
||||
}
|
||||
.pufs > i:after {
|
||||
content: url('data:image/svg+xml; utf8, <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.6875 0.6875C1.75403 0.6875 0.1875 2.25403 0.1875 4.1875C0.1875 6.12097 1.75403 7.6875 3.6875 7.6875C5.62097 7.6875 7.1875 6.12097 7.1875 4.1875C7.1875 2.25403 5.62097 0.6875 3.6875 0.6875Z" fill="%235851D8"/></svg>');
|
||||
content: url('data:image/svg+xml; utf8, <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.6875 0.6875C1.75403 0.6875 0.1875 2.25403 0.1875 4.1875C0.1875 6.12097 1.75403 7.6875 3.6875 7.6875C5.62097 7.6875 7.1875 6.12097 7.1875 4.1875C7.1875 2.25403 5.62097 0.6875 3.6875 0.6875Z" fill="%239EA9C4"/></svg>');
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
position: relative;
|
||||
@ -513,7 +517,7 @@ export default {
|
||||
position: absolute;
|
||||
}
|
||||
.particles > i:after {
|
||||
content: url('data:image/svg+xml; utf8, <svg width="3" height="3" viewBox="0 0 3 3" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.1875 0.6875C0.635081 0.6875 0.1875 1.13508 0.1875 1.6875C0.1875 2.23992 0.635081 2.6875 1.1875 2.6875C1.73992 2.6875 2.1875 2.23992 2.1875 1.6875C2.1875 1.13508 1.73992 0.6875 1.1875 0.6875Z" fill="%235851D8"/></svg>');
|
||||
content: url('data:image/svg+xml; utf8, <svg width="3" height="3" viewBox="0 0 3 3" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.1875 0.6875C0.635081 0.6875 0.1875 1.13508 0.1875 1.6875C0.1875 2.23992 0.635081 2.6875 1.1875 2.6875C1.73992 2.6875 2.1875 2.23992 2.1875 1.6875C2.1875 1.13508 1.73992 0.6875 1.1875 0.6875Z" fill="%239EA9C4"/></svg>');
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
position: relative;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<component v-if="isLoaded" :is="heroIcons[name]" class="h-5 w-5" />
|
||||
<component :is="heroIcons[name]" v-if="isLoaded" class="h-5 w-5" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -11,6 +11,7 @@ const isLoaded = ref(false)
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<BaseIcon
|
||||
name="ExclamationIcon"
|
||||
class="h-5 w-5 text-yellow-400"
|
||||
|
||||
@ -10,7 +10,7 @@ const props = defineProps({
|
||||
|
||||
const formLayout = computed(() => {
|
||||
if (props.layout === 'two-column') {
|
||||
return 'grid gap-y-6 gap-x-4 md:grid-cols-2'
|
||||
return 'grid gap-y-6 gap-x-4 grid-cols-1 md:grid-cols-2'
|
||||
}
|
||||
|
||||
return 'grid gap-y-6 gap-x-4 grid-cols-1'
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
not-italic
|
||||
items-center
|
||||
font-medium
|
||||
text-primary-800
|
||||
text-gray-800
|
||||
whitespace-nowrap
|
||||
justify-between
|
||||
"
|
||||
@ -31,7 +31,7 @@
|
||||
</label>
|
||||
<div :class="inputContainerClasses">
|
||||
<slot></slot>
|
||||
<span v-if="helpText" class="text-gray-400 text-xs mt-1 font-light">
|
||||
<span v-if="helpText" class="text-gray-500 text-xs mt-1 font-light">
|
||||
{{ helpText }}
|
||||
</span>
|
||||
<span v-if="error" class="block mt-0.5 text-sm text-red-500">
|
||||
|
||||
@ -83,12 +83,12 @@
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { useItemStore } from '@/scripts/stores/item'
|
||||
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
||||
import { useItemStore } from '@/scripts/admin/stores/item'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import abilities from '@/scripts/stub/abilities'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
contentLoading: {
|
||||
|
||||
@ -58,8 +58,8 @@
|
||||
rounded-lg
|
||||
text-left
|
||||
overflow-hidden
|
||||
relative
|
||||
shadow-xl
|
||||
transform
|
||||
transition-all
|
||||
my-4
|
||||
${modalSize}
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { Money3Component } from 'v-money3'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
||||
|
||||
let money3 = Money3Component
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
type: [String],
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
200
resources/scripts/components/base/BaseRating.vue
Normal file
200
resources/scripts/components/base/BaseRating.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="star-rating">
|
||||
<div
|
||||
v-for="(star, index) in stars"
|
||||
:key="index"
|
||||
:title="rating"
|
||||
class="star-container"
|
||||
>
|
||||
<svg
|
||||
:style="[
|
||||
{ fill: `url(#gradient${star.raw})` },
|
||||
{ width: style.starWidth },
|
||||
{ height: style.starHeight },
|
||||
]"
|
||||
class="star-svg"
|
||||
>
|
||||
<polygon :points="getStarPoints" style="fill-rule: nonzero" />
|
||||
<defs>
|
||||
<!--
|
||||
id has to be unique to each star fullness(dynamic offset) - it indicates fullness above
|
||||
-->
|
||||
<linearGradient :id="`gradient${star.raw}`">
|
||||
<stop
|
||||
id="stop1"
|
||||
:offset="star.percent"
|
||||
:stop-color="getFullFillColor(star)"
|
||||
stop-opacity="1"
|
||||
></stop>
|
||||
<stop
|
||||
id="stop2"
|
||||
:offset="star.percent"
|
||||
:stop-color="getFullFillColor(star)"
|
||||
stop-opacity="0"
|
||||
></stop>
|
||||
<stop
|
||||
id="stop3"
|
||||
:offset="star.percent"
|
||||
:stop-color="style.emptyStarColor"
|
||||
stop-opacity="1"
|
||||
></stop>
|
||||
<stop
|
||||
id="stop4"
|
||||
:stop-color="style.emptyStarColor"
|
||||
offset="100%"
|
||||
stop-opacity="1"
|
||||
></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div v-if="isIndicatorActive" class="indicator">{{ rating }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StarsRating',
|
||||
components: {},
|
||||
directives: {},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
rating: {
|
||||
type: [Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
stars: [],
|
||||
emptyStar: 0,
|
||||
fullStar: 1,
|
||||
totalStars: 5,
|
||||
isIndicatorActive: false,
|
||||
style: {
|
||||
fullStarColor: '#F1C644',
|
||||
emptyStarColor: '#D4D4D4',
|
||||
starWidth: 20,
|
||||
starHeight: 20,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getStarPoints: function () {
|
||||
let centerX = this.style.starWidth / 2
|
||||
let centerY = this.style.starHeight / 2
|
||||
|
||||
let innerCircleArms = 5 // a 5 arms star
|
||||
|
||||
let innerRadius = this.style.starWidth / innerCircleArms
|
||||
let innerOuterRadiusRatio = 2.5 // Unique value - determines fatness/sharpness of star
|
||||
let outerRadius = innerRadius * innerOuterRadiusRatio
|
||||
|
||||
return this.calcStarPoints(
|
||||
centerX,
|
||||
centerY,
|
||||
innerCircleArms,
|
||||
innerRadius,
|
||||
outerRadius
|
||||
)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initStars()
|
||||
this.setStars()
|
||||
this.setConfigData()
|
||||
},
|
||||
methods: {
|
||||
calcStarPoints(
|
||||
centerX,
|
||||
centerY,
|
||||
innerCircleArms,
|
||||
innerRadius,
|
||||
outerRadius
|
||||
) {
|
||||
let angle = Math.PI / innerCircleArms
|
||||
let angleOffsetToCenterStar = 60
|
||||
|
||||
let totalArms = innerCircleArms * 2
|
||||
let points = ''
|
||||
for (let i = 0; i < totalArms; i++) {
|
||||
let isEvenIndex = i % 2 == 0
|
||||
let r = isEvenIndex ? outerRadius : innerRadius
|
||||
let currX = centerX + Math.cos(i * angle + angleOffsetToCenterStar) * r
|
||||
let currY = centerY + Math.sin(i * angle + angleOffsetToCenterStar) * r
|
||||
points += currX + ',' + currY + ' '
|
||||
}
|
||||
return points
|
||||
},
|
||||
initStars() {
|
||||
for (let i = 0; i < this.totalStars; i++) {
|
||||
this.stars.push({
|
||||
raw: this.emptyStar,
|
||||
percent: this.emptyStar + '%',
|
||||
})
|
||||
}
|
||||
},
|
||||
setStars() {
|
||||
let fullStarsCounter = Math.floor(this.rating)
|
||||
for (let i = 0; i < this.stars.length; i++) {
|
||||
if (fullStarsCounter !== 0) {
|
||||
this.stars[i].raw = this.fullStar
|
||||
this.stars[i].percent = this.calcStarFullness(this.stars[i])
|
||||
fullStarsCounter--
|
||||
} else {
|
||||
let surplus = Math.round((this.rating % 1) * 10) / 10 // Support just one decimal
|
||||
let roundedOneDecimalPoint = Math.round(surplus * 10) / 10
|
||||
this.stars[i].raw = roundedOneDecimalPoint
|
||||
return (this.stars[i].percent = this.calcStarFullness(this.stars[i]))
|
||||
}
|
||||
}
|
||||
},
|
||||
setConfigData() {
|
||||
if (this.config) {
|
||||
this.setBindedProp(this.style, this.config.style, 'fullStarColor')
|
||||
this.setBindedProp(this.style, this.config.style, 'emptyStarColor')
|
||||
this.setBindedProp(this.style, this.config.style, 'starWidth')
|
||||
this.setBindedProp(this.style, this.config.style, 'starHeight')
|
||||
if (this.config.isIndicatorActive) {
|
||||
this.isIndicatorActive = this.config.isIndicatorActive
|
||||
}
|
||||
console.log('isIndicatorActive: ', this.isIndicatorActive)
|
||||
}
|
||||
},
|
||||
getFullFillColor(starData) {
|
||||
return starData.raw !== this.emptyStar
|
||||
? this.style.fullStarColor
|
||||
: this.style.emptyStarColor
|
||||
},
|
||||
calcStarFullness(starData) {
|
||||
let starFullnessPercent = starData.raw * 100 + '%'
|
||||
return starFullnessPercent
|
||||
},
|
||||
setBindedProp(localProp, propParent, propToBind) {
|
||||
if (propParent[propToBind]) {
|
||||
localProp[propToBind] = propParent[propToBind]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.star-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.star-container {
|
||||
display: flex;
|
||||
.star-svg {
|
||||
}
|
||||
}
|
||||
.indicator {
|
||||
}
|
||||
.star-container:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -12,7 +12,7 @@
|
||||
>
|
||||
<ListboxLabel
|
||||
v-if="label"
|
||||
class="block text-sm not-italic font-medium text-primary-800 mb-0.5"
|
||||
class="block text-sm not-italic font-medium text-gray-800 mb-0.5"
|
||||
>
|
||||
{{ label }}
|
||||
</ListboxLabel>
|
||||
@ -33,7 +33,8 @@
|
||||
shadow-sm
|
||||
cursor-default
|
||||
focus:outline-none
|
||||
focus:ring-1 focus:ring-primary-500
|
||||
focus:ring-1
|
||||
focus:ring-primary-500
|
||||
focus:border-primary-500
|
||||
sm:text-sm
|
||||
"
|
||||
@ -120,7 +121,6 @@
|
||||
]"
|
||||
>
|
||||
<BaseIcon name="CheckIcon" aria-hidden="true" />
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
|
||||
23
resources/scripts/components/base/BaseSpinner.vue
Normal file
23
resources/scripts/components/base/BaseSpinner.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<svg
|
||||
class="animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
w-4
|
||||
h-4
|
||||
transition-transform
|
||||
transform
|
||||
bg-white
|
||||
rounded-full
|
||||
"
|
||||
|
||||
@ -15,10 +15,11 @@
|
||||
</SwitchDescription>
|
||||
</div>
|
||||
<Switch
|
||||
:disabled="disabled"
|
||||
:model-value="modelValue"
|
||||
:class="[
|
||||
modelValue ? 'bg-primary-500' : 'bg-gray-200',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500',
|
||||
'ml-4 relative inline-flex shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
]"
|
||||
@update:modelValue="onUpdate"
|
||||
>
|
||||
@ -26,7 +27,7 @@
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
modelValue ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition ease-in-out duration-200',
|
||||
]"
|
||||
/>
|
||||
</Switch>
|
||||
@ -54,6 +55,10 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
Reference in New Issue
Block a user