mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-17 10:14:08 +00:00
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.
278 lines
9.4 KiB
Vue
278 lines
9.4 KiB
Vue
<template>
|
|
<div ref="companySwitchBar" class="relative rounded">
|
|
<div
|
|
class="
|
|
flex items-center justify-center px-3 h-8 md:h-9 ml-2 text-sm text-white
|
|
bg-white/20 rounded-lg cursor-pointer hover:bg-white/30 transition-colors
|
|
"
|
|
@click="isShow = !isShow"
|
|
>
|
|
<span
|
|
v-if="companyStore.isAdminMode"
|
|
class="w-16 text-sm font-medium truncate sm:w-auto"
|
|
>
|
|
{{ $t('navigation.administration') }}
|
|
</span>
|
|
<span
|
|
v-else-if="companyStore.selectedCompany"
|
|
class="w-16 text-sm font-medium truncate sm:w-auto"
|
|
>
|
|
{{ companyStore.selectedCompany.name }}
|
|
</span>
|
|
<BaseIcon name="ChevronDownIcon" class="h-5 ml-1 text-white" />
|
|
</div>
|
|
|
|
<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"
|
|
>
|
|
<div
|
|
v-if="isShow"
|
|
class="absolute right-0 mt-2 bg-surface rounded-md shadow-lg"
|
|
>
|
|
<div
|
|
class="
|
|
overflow-y-auto scrollbar-thin scrollbar-thumb-rounded-full
|
|
w-[300px] max-h-[350px]
|
|
scrollbar-thumb-surface-muted scrollbar-track-surface-secondary pb-4
|
|
"
|
|
>
|
|
<!-- Administration Mode -->
|
|
<div v-if="userStore.currentUser?.is_super_admin">
|
|
<div
|
|
class="p-2 px-3 rounded-md cursor-pointer hover:bg-hover-strong hover:text-primary-500"
|
|
:class="{
|
|
'bg-surface-tertiary text-primary-500': companyStore.isAdminMode,
|
|
}"
|
|
@click="enterAdminMode"
|
|
>
|
|
<div class="flex items-center">
|
|
<span
|
|
class="flex items-center justify-center mr-3 overflow-hidden text-base font-semibold bg-primary-100 rounded-md w-9 h-9 shrink-0 text-primary-500"
|
|
>
|
|
<BaseIcon name="ShieldCheckIcon" class="w-5 h-5" />
|
|
</span>
|
|
<div class="flex flex-col">
|
|
<span class="text-sm font-medium">{{ $t('navigation.administration') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-line-light my-1" />
|
|
</div>
|
|
|
|
<label
|
|
class="px-3 py-2 text-xs font-semibold text-subtle mb-0.5 block uppercase"
|
|
>
|
|
{{ $t('company_switcher.label') }}
|
|
</label>
|
|
|
|
<div
|
|
v-if="companyStore.companies.length < 1"
|
|
class="flex flex-col items-center justify-center p-2 px-3 mt-4 text-base text-subtle"
|
|
>
|
|
<BaseIcon name="ExclamationCircleIcon" class="h-5 text-subtle" />
|
|
{{ $t('company_switcher.no_results_found') }}
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div v-if="companyStore.companies.length > 0">
|
|
<div
|
|
v-for="(company, index) in companyStore.companies"
|
|
:key="index"
|
|
class="p-2 px-3 rounded-md cursor-pointer hover:bg-hover-strong hover:text-primary-500"
|
|
:class="{
|
|
'bg-surface-tertiary text-primary-500':
|
|
companyStore.selectedCompany && companyStore.selectedCompany.id === company.id,
|
|
}"
|
|
@click="changeCompany(company)"
|
|
>
|
|
<div class="flex items-center">
|
|
<span
|
|
class="
|
|
flex items-center justify-center mr-3 overflow-hidden text-base font-semibold
|
|
bg-surface-muted rounded-md w-9 h-9 text-primary-500
|
|
"
|
|
>
|
|
<span v-if="!company.logo">
|
|
{{ initGenerator(company.name) }}
|
|
</span>
|
|
<img
|
|
v-else
|
|
:src="company.logo"
|
|
alt="Company logo"
|
|
class="w-full h-full object-contain"
|
|
/>
|
|
</span>
|
|
<div class="flex flex-col">
|
|
<span class="text-sm">{{ company.name }}</span>
|
|
<span v-if="company.user_role" class="text-xs text-subtle">
|
|
{{ company.user_role }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pending Invitations -->
|
|
<div
|
|
v-if="pendingInvitations.length > 0"
|
|
class="border-t border-line-light p-2"
|
|
>
|
|
<label
|
|
class="block px-1 pt-1 pb-2 text-xs font-semibold leading-tight text-subtle uppercase"
|
|
>
|
|
{{ $t('members.pending_invitations') }}
|
|
</label>
|
|
<div
|
|
v-for="invitation in pendingInvitations"
|
|
:key="invitation.id"
|
|
class="p-2 px-3 rounded-md"
|
|
>
|
|
<div class="flex items-center mb-2">
|
|
<span
|
|
class="
|
|
flex items-center justify-center mr-3 overflow-hidden text-xs font-semibold
|
|
bg-surface-muted rounded-md w-9 h-9 shrink-0 text-subtle
|
|
"
|
|
>
|
|
{{ initGenerator(invitation.company?.name ?? '?') }}
|
|
</span>
|
|
<div class="flex flex-col min-w-0">
|
|
<span class="text-sm text-body truncate">{{ invitation.company?.name }}</span>
|
|
<span class="text-xs text-subtle">{{ invitation.role?.title }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-1 pl-12">
|
|
<button
|
|
class="text-xs px-2 py-1 rounded bg-primary-500 text-white hover:bg-primary-600"
|
|
@click.stop="acceptInvitation(invitation.token)"
|
|
>
|
|
{{ $t('general.accept') }}
|
|
</button>
|
|
<button
|
|
class="text-xs px-2 py-1 rounded bg-surface-muted text-body hover:bg-hover-strong"
|
|
@click.stop="declineInvitation(invitation.token)"
|
|
>
|
|
{{ $t('general.decline') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="userStore.currentUser?.is_owner"
|
|
class="
|
|
flex items-center justify-center p-4 pl-3 border-t-2 border-line-light
|
|
cursor-pointer text-primary-400 hover:text-primary-500
|
|
"
|
|
@click="addNewCompany"
|
|
>
|
|
<BaseIcon name="PlusIcon" class="h-5 mr-2" />
|
|
|
|
<span class="font-medium">
|
|
{{ $t('company_switcher.add_new_company') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<CompanyModal />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch } from 'vue'
|
|
import { onClickOutside } from '@vueuse/core'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useCompanyStore } from '@/scripts/stores/company.store'
|
|
import { useGlobalStore } from '@/scripts/stores/global.store'
|
|
import { useUserStore } from '@/scripts/stores/user.store'
|
|
import { useModalStore } from '@/scripts/stores/modal.store'
|
|
import CompanyModal from '@/scripts/features/company/settings/components/CompanyModal.vue'
|
|
import type { Company, CompanyInvitation } from '@/scripts/types/domain/company'
|
|
import type { Role } from '@/scripts/types/domain/role'
|
|
|
|
interface PendingInvitation {
|
|
id: number
|
|
token: string
|
|
company?: { name: string }
|
|
role?: { title: string }
|
|
}
|
|
|
|
const companyStore = useCompanyStore()
|
|
const modalStore = useModalStore()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const globalStore = useGlobalStore()
|
|
const { t } = useI18n()
|
|
const userStore = useUserStore()
|
|
|
|
const isShow = ref<boolean>(false)
|
|
const companySwitchBar = ref<HTMLElement | null>(null)
|
|
|
|
// TODO: Wire up pending invitations from a dedicated invitation store or bootstrap data
|
|
const pendingInvitations = ref<PendingInvitation[]>([])
|
|
|
|
watch(route, () => {
|
|
isShow.value = false
|
|
})
|
|
|
|
onClickOutside(companySwitchBar, () => {
|
|
isShow.value = false
|
|
})
|
|
|
|
function initGenerator(name: string): string {
|
|
if (name) {
|
|
const nameSplit = name.split(' ')
|
|
return nameSplit[0].charAt(0).toUpperCase()
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function addNewCompany(): void {
|
|
modalStore.openModal({
|
|
title: t('company_switcher.new_company'),
|
|
componentName: 'CompanyModal',
|
|
size: 'sm',
|
|
})
|
|
}
|
|
|
|
async function enterAdminMode(): Promise<void> {
|
|
companyStore.setAdminMode(true)
|
|
isShow.value = false
|
|
router.push('/admin/administration/dashboard')
|
|
globalStore.setIsAppLoaded(false)
|
|
await globalStore.bootstrap()
|
|
}
|
|
|
|
async function changeCompany(company: Company): Promise<void> {
|
|
companyStore.setAdminMode(false)
|
|
companyStore.setSelectedCompany(company)
|
|
router.push('/admin/dashboard')
|
|
globalStore.setIsAppLoaded(false)
|
|
await globalStore.bootstrap()
|
|
}
|
|
|
|
async function acceptInvitation(token: string): Promise<void> {
|
|
// TODO: call invitation accept API, then re-bootstrap
|
|
pendingInvitations.value = pendingInvitations.value.filter(
|
|
(inv) => inv.token !== token
|
|
)
|
|
await globalStore.bootstrap()
|
|
}
|
|
|
|
async function declineInvitation(token: string): Promise<void> {
|
|
// TODO: call invitation decline API
|
|
pendingInvitations.value = pendingInvitations.value.filter(
|
|
(inv) => inv.token !== token
|
|
)
|
|
}
|
|
</script>
|