Moved to _dev

This commit is contained in:
2025-09-20 16:11:47 +02:00
parent fb1a8753b7
commit b2ba11fcd3
1670 changed files with 224899 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
<template>
<div v-if="isAppLoaded" class="h-full">
<NotificationRoot />
<SiteHeader />
<SiteSidebar />
<ExchangeRateBulkUpdateModal />
<main
class="h-screen h-screen-ios overflow-y-auto md:pl-56 xl:pl-64 min-h-0"
>
<div class="pt-16 pb-16">
<router-view />
</div>
</main>
</div>
<BaseGlobalLoader v-else />
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { useGlobalStore } from '@/scripts/admin/stores/global'
import { onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/scripts/admin/stores/user'
import { useModalStore } from '@/scripts/stores/modal'
import { useExchangeRateStore } from '@/scripts/admin/stores/exchange-rate'
import { useCompanyStore } from '@/scripts/admin/stores/company'
import SiteHeader from '@/scripts/admin/layouts/partials/TheSiteHeader.vue'
import SiteSidebar from '@/scripts/admin/layouts/partials/TheSiteSidebar.vue'
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
import ExchangeRateBulkUpdateModal from '@/scripts/admin/components/modal-components/ExchangeRateBulkUpdateModal.vue'
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>

View File

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

View File

@@ -0,0 +1,172 @@
<template>
<div class="grid h-screen 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
v-if="!loginPageLogo"
class="block w-48 h-auto max-w-full mb-32 text-primary-500"
/>
<img
v-else
:src="loginPageLogo"
class="block w-48 h-auto max-w-full mb-32 text-primary-500"
/>
<router-view />
<div
class="
pt-24
mt-0
text-sm
not-italic
font-medium
leading-relaxed
text-left text-gray-400
md:pt-40
"
>
<p class="mb-3">
{{ copyrightText }}
{{ 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="md:pl-10 xl:pl-0 relative z-50 w-7/12 xl:w-5/12 xl:w-5/12">
<h1
class="
hidden
mb-3
text-3xl
leading-normal
text-left text-white
xl:text-5xl xl:leading-tight
md:none
lg:block
"
>
{{ pageHeading }}
</h1>
<p
class="
hidden
text-sm
not-italic
font-normal
leading-normal
text-left text-gray-100
xl:text-base xl:leading-6
md:none
lg:block
"
>
{{ pageDescription }}
</p>
</div>
<LoginBottomVector
class="
absolute
z-50
w-full
bg-no-repeat
content-bottom
h-[15vw]
lg:h-[22vw]
right-[32%]
bottom-0
"
/>
</div>
</div>
</template>
<script setup>
import NotificationRoot from '@/scripts/components/notifications/NotificationRoot.vue'
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
import LoginBackground from '@/scripts/components/svg/LoginBackground.vue'
import LoginPlanetCrater from '@/scripts/components/svg/LoginPlanetCrater.vue'
import LoginBottomVector from '@/scripts/components/svg/LoginBottomVector.vue'
import LoginBackgroundOverlay from '@/scripts/components/svg/LoginBackgroundOverlay.vue'
import { computed, ref } from 'vue'
const pageHeading = computed(() => {
if (window.login_page_heading) {
return window.login_page_heading
}
return 'Simple Invoicing for Individuals Small Businesses'
})
const pageDescription = computed(() => {
if (window.login_page_description) {
return window.login_page_description
}
return 'Crater helps you track expenses, record payments & generate beautiful invoices & estimates.'
})
const copyrightText = computed(() => {
if (window.copyright_text) {
return window.copyright_text
}
return 'Copyright @ Crater Invoice, Inc.'
})
const loginPageLogo = computed(() => {
if (window.login_page_logo) {
return window.login_page_logo
}
return false
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,228 @@
<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 v-if="adminLogo" :src="adminLogo" class="h-6" />
<MainLogo v-else 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 object-cover"
/>
</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()
})
const adminLogo = computed(() => {
if (globalStore.globalSettings.admin_portal_logo) {
return '/storage/' + globalStore.globalSettings.admin_portal_logo
}
return false
})
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>

View 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
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>