Files
InvoiceShelf/resources/scripts/layouts/partials/CompanySwitcher.vue
Darko Gjorgjijoski 71388ec6a5 Rename resources/scripts-v2 to resources/scripts and drop @v2 alias
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.
2026-04-07 12:50:16 +02:00

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>