mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 12:11:08 -04:00
v5.0.0 update
This commit is contained in:
88
resources/scripts/layouts/LayoutBasic.vue
Normal file
88
resources/scripts/layouts/LayoutBasic.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div v-if="isAppLoaded" class="h-full">
|
||||
<NotificationRoot />
|
||||
|
||||
<SiteHeader />
|
||||
|
||||
<SiteSidebar />
|
||||
|
||||
<ExchangeRateBulkUpdateModal />
|
||||
|
||||
<main
|
||||
class="
|
||||
mt-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/stores/global'
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useExchangeRateStore } from '@/scripts/stores/exchange-rate'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
|
||||
import SiteHeader from '@/scripts/layouts/partials/TheSiteHeader.vue'
|
||||
import SiteSidebar from '@/scripts/layouts/partials/TheSiteSidebar.vue'
|
||||
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
|
||||
import ExchangeRateBulkUpdateModal from '@/scripts/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/layouts/LayoutInstallation.vue
Normal file
13
resources/scripts/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>
|
||||
151
resources/scripts/layouts/LayoutLogin.vue
Normal file
151
resources/scripts/layouts/LayoutLogin.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<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">
|
||||
<img
|
||||
src="/img/crater-logo.png"
|
||||
class="block w-48 h-auto max-w-full mb-32 text-primary-400"
|
||||
alt="Crater Logo"
|
||||
/>
|
||||
|
||||
<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
|
||||
"
|
||||
>
|
||||
<div class="pl-20 xl:pl-0">
|
||||
<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>
|
||||
<div class="absolute z-50 w-full bg-no-repeat content-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-box {
|
||||
background-image: url('/img/login/login-vector1.svg');
|
||||
}
|
||||
.content-bottom {
|
||||
background-image: url('/img/login/login-vector3.svg');
|
||||
background-size: 100% 100%;
|
||||
height: 300px;
|
||||
right: 32%;
|
||||
bottom: 0;
|
||||
}
|
||||
.content-box::before {
|
||||
background-image: url('/img/login/frame.svg');
|
||||
content: '';
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
height: 300px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 420px;
|
||||
z-index: 1;
|
||||
}
|
||||
.content-box::after {
|
||||
background-image: url('/img/login/login-vector2.svg');
|
||||
content: '';
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
right: 7.5%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
170
resources/scripts/layouts/partials/TheSiteHeader.vue
Normal file
170
resources/scripts/layouts/partials/TheSiteHeader.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<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"
|
||||
>
|
||||
<img
|
||||
id="logo-white"
|
||||
src="/img/logo-white.png"
|
||||
alt="Crater Logo"
|
||||
class="h-6"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
Logout
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/scripts/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
import CompanySwitcher from '@/scripts/components/CompanySwitcher.vue'
|
||||
import GlobalSearchBar from '@/scripts/components/GlobalSearchBar.vue'
|
||||
import abilities from '@/scripts/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>
|
||||
178
resources/scripts/layouts/partials/TheSiteSidebar.vue
Normal file
178
resources/scripts/layouts/partials/TheSiteSidebar.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<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 transform"
|
||||
enter-from="-translate-x-full"
|
||||
enter-to="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
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 flex-shrink-0 px-4 mb-10">
|
||||
<img
|
||||
src="/img/crater-logo.png"
|
||||
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 flex-shrink-0 h-5 w-5',
|
||||
]"
|
||||
@click="globalStore.setSidebarVisibility(false)"
|
||||
/>
|
||||
{{ $t(item.title) }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
<div class="flex-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 flex-shrink-0 h-5 w-5 ',
|
||||
]"
|
||||
/>
|
||||
|
||||
{{ $t(item.title) }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from '@headlessui/vue'
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
|
||||
const route = useRoute()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
function hasActiveUrl(url) {
|
||||
return route.path.indexOf(url) > -1
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user