changes 0328092025
This commit is contained in:
@@ -16,6 +16,11 @@ const props = defineProps({
|
||||
description: String,
|
||||
header: Array,
|
||||
body: Array,
|
||||
// Make table header sticky while body scrolls
|
||||
stickyHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
editor: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -115,12 +120,16 @@ const remove = () => {
|
||||
<p v-if="description" class="mt-1 text-sm text-gray-600">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div :class="['relative rounded-lg border border-gray-200 bg-white shadow-sm overflow-x-auto overflow-y-auto', bodyMaxHeight]">
|
||||
<div :class="['relative rounded-lg border border-gray-200 bg-white shadow-sm overflow-x-auto overflow-y-auto', bodyMaxHeight, stickyHeader ? 'table-sticky' : '']">
|
||||
<FwbTable hoverable striped class="text-sm">
|
||||
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur supports-[backdrop-filter]:bg-gray-50/80 border-b border-gray-200 shadow-sm">
|
||||
<FwbTableHeadCell v-for="(h, hIndex) in header" :key="hIndex" class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6">{{ h.data }}</FwbTableHeadCell>
|
||||
<FwbTableHeadCell v-if="editor" class="w-px text-gray-700 py-3"></FwbTableHeadCell>
|
||||
<FwbTableHeadCell v-else class="w-px text-gray-700 py-3" />
|
||||
<FwbTableHeadCell
|
||||
v-for="(h, hIndex) in header"
|
||||
:key="hIndex"
|
||||
class="sticky top-0 z-10 uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6 bg-gray-50/90"
|
||||
>{{ h.data }}</FwbTableHeadCell>
|
||||
<FwbTableHeadCell v-if="editor" class="sticky top-0 z-10 w-px text-gray-700 py-3 bg-gray-50/90"></FwbTableHeadCell>
|
||||
<FwbTableHeadCell v-else class="sticky top-0 z-10 w-px text-gray-700 py-3 bg-gray-50/90" />
|
||||
</FwbTableHead>
|
||||
<FwbTableBody>
|
||||
<FwbTableRow v-for="(row, key, parent_index) in body" :key="key" :class="row.options.class">
|
||||
@@ -208,4 +217,27 @@ const remove = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Ensure sticky header remains above scrollable body inside wrapper */
|
||||
:deep(.table-sticky thead) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:deep(.table-sticky thead th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(249, 250, 251, 0.9); /* gray-50/90 */
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
}
|
||||
|
||||
/* Maintain column widths alignment when scrollbar appears */
|
||||
.table-sticky {
|
||||
/* Make sure the header and body share the same scroll container */
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,9 @@ import { ref, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
postUrl: { type: String, required: true },
|
||||
// Optional list of contracts to allow attaching the document directly to a contract
|
||||
// Each item should have at least: { uuid, reference }
|
||||
contracts: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['close', 'uploaded'])
|
||||
|
||||
@@ -21,7 +24,8 @@ const form = useForm({
|
||||
name: '',
|
||||
description: '',
|
||||
file: null,
|
||||
is_public: false,
|
||||
is_public: true,
|
||||
contract_uuid: null,
|
||||
})
|
||||
const localError = ref('')
|
||||
|
||||
@@ -86,6 +90,13 @@ const close = () => emit('close')
|
||||
<template #title>Dodaj dokument</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div v-if="props.contracts && props.contracts.length" class="grid grid-cols-1 gap-2">
|
||||
<InputLabel for="doc_attach" value="Pripiši k" />
|
||||
<select id="doc_attach" v-model="form.contract_uuid" class="mt-1 block w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option :value="null">Primer</option>
|
||||
<option v-for="c in props.contracts" :key="c.uuid" :value="c.uuid">Pogodba: {{ c.reference }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="doc_name" value="Name" />
|
||||
<TextInput id="doc_name" class="mt-1 block w-full" v-model="form.name" />
|
||||
|
||||
@@ -11,6 +11,15 @@ const props = defineProps({
|
||||
// Optional: build a direct download URL for a document; if not provided, a 'download' event will be emitted
|
||||
downloadUrlBuilder: { type: Function, default: null },
|
||||
})
|
||||
// Derive a human-friendly source for a document: Case or Contract reference
|
||||
const sourceLabel = (doc) => {
|
||||
// Server can include optional documentable meta; fall back to type
|
||||
if (doc.documentable_type?.toLowerCase?.().includes('contract')) {
|
||||
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : 'Pogodba'
|
||||
}
|
||||
return 'Primer'
|
||||
}
|
||||
|
||||
const emit = defineEmits(['view', 'download'])
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
@@ -110,11 +119,11 @@ const handleDownload = (doc) => {
|
||||
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<FwbTable hoverable striped class="text-sm">
|
||||
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm">
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Name</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Type</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Size</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Added</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Drugo</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Naziv</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vrsta</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Velikost</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Dodano</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vir</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="w-px" />
|
||||
</FwbTableHead>
|
||||
<FwbTableBody>
|
||||
@@ -122,7 +131,7 @@ const handleDownload = (doc) => {
|
||||
<FwbTableRow>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="text-indigo-600 hover:underline" @click="$emit('view', doc)">{{ doc.original_name || doc.name }}</button>
|
||||
<button type="button" class="text-indigo-600 hover:underline" @click="$emit('view', doc)">{{ doc.name }}</button>
|
||||
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
@@ -134,6 +143,9 @@ const handleDownload = (doc) => {
|
||||
</FwbTableCell>
|
||||
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
|
||||
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-center">
|
||||
<button
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
person: { type: Object, required: true },
|
||||
types: { type: Object, default: () => ({}) },
|
||||
// Allow overriding the default active tab: 'addresses' | 'phones' | 'emails' | 'bank'
|
||||
defaultTab: { type: String, default: 'addresses' },
|
||||
})
|
||||
|
||||
const phoneTypes = computed(() => {
|
||||
const arr = props.types?.phone_types || []
|
||||
const map = {}
|
||||
for (const t of arr) { map[t.id] = t.name }
|
||||
return map
|
||||
})
|
||||
|
||||
const displayName = computed(() => {
|
||||
const p = props.person || {}
|
||||
const full = p.full_name?.trim()
|
||||
if (full) { return full }
|
||||
const first = p.first_name?.trim() || ''
|
||||
const last = p.last_name?.trim() || ''
|
||||
return `${first} ${last}`.trim()
|
||||
})
|
||||
|
||||
const primaryAddress = computed(() => props.person?.addresses?.[0] || null)
|
||||
const primaryEmail = computed(() => props.person?.emails?.[0]?.value || null)
|
||||
// Backend phone model uses `nu` as the number
|
||||
const allPhones = computed(() => props.person?.phones || [])
|
||||
const allAddresses = computed(() => props.person?.addresses || [])
|
||||
const allEmails = computed(() => props.person?.emails || [])
|
||||
// Laravel serializes relation names to snake_case, so prefer bank_accounts, fallback to bankAccounts
|
||||
const allBankAccounts = computed(() => props.person?.bank_accounts || props.person?.bankAccounts || [])
|
||||
const bankIban = computed(() => allBankAccounts.value?.[0]?.iban || null)
|
||||
const taxNumber = computed(() => props.person?.tax_number || null)
|
||||
const ssn = computed(() => props.person?.social_security_number || null)
|
||||
|
||||
// Summary sizing
|
||||
const showMore = ref(false)
|
||||
const summaryPhones = computed(() => allPhones.value.slice(0, showMore.value ? 2 : 1))
|
||||
|
||||
// Tabs
|
||||
const activeTab = ref(props.defaultTab || 'addresses')
|
||||
watch(() => props.defaultTab, (val) => { if (val) activeTab.value = val })
|
||||
|
||||
function maskIban(iban) {
|
||||
if (!iban || typeof iban !== 'string') return null
|
||||
const clean = iban.replace(/\s+/g, '')
|
||||
if (clean.length <= 8) return clean
|
||||
return `${clean.slice(0, 4)} **** **** ${clean.slice(-4)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-sm">
|
||||
<div v-if="displayName" class="font-medium text-gray-900">{{ displayName }}</div>
|
||||
|
||||
<div v-if="primaryAddress" class="mt-1 text-gray-700">
|
||||
<span>{{ primaryAddress.address }}</span>
|
||||
<span v-if="primaryAddress.country" class="text-gray-500 text-xs ml-1">({{ primaryAddress.country }})</span>
|
||||
</div>
|
||||
|
||||
<div v-if="summaryPhones?.length" class="mt-1 space-y-0.5">
|
||||
<div v-for="p in summaryPhones" :key="p.id" class="text-gray-700">
|
||||
<span>{{ p.nu }}</span>
|
||||
<span v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name" class="text-gray-500 text-xs ml-1">({{ p.type?.name || phoneTypes[p.type_id] }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMore && primaryEmail" class="mt-1 text-gray-700">{{ primaryEmail }}</div>
|
||||
|
||||
<div v-if="showMore && bankIban" class="mt-1 text-gray-700">TRR: <span class="font-mono">{{ maskIban(bankIban) }}</span></div>
|
||||
|
||||
<div v-if="showMore && taxNumber" class="mt-1 text-gray-700">Davčna: <span class="font-mono">{{ taxNumber }}</span></div>
|
||||
<div v-if="showMore && ssn" class="mt-1 text-gray-700">EMŠO: <span class="font-mono">{{ ssn }}</span></div>
|
||||
|
||||
<button type="button" class="mt-2 text-xs text-blue-600 hover:underline" @click="showMore = !showMore">
|
||||
{{ showMore ? 'Skrij' : 'Prikaži več' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="mt-3">
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
<button type="button" @click="activeTab = 'addresses'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='addresses' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a1 1 0 0 1 .832.445l6 8.5a1 1 0 0 1 .168.555V17a1 1 0 0 1-1 1h-4v-4H8v4H4a1 1 0 0 1-1-1v-5.5a1 1 0 0 1 .168-.555l6-8.5A1 1 0 0 1 10 2Z"/></svg>
|
||||
Naslovi ({{ allAddresses.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'phones'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='phones' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M2.3 3.3c.6-1 1.9-1.3 2.9-.7l1.7 1a2 2 0 0 1 .9 2.5l-.5 1.2a2 2 0 0 0 .4 2.2l2.8 2.8a2 2 0 0 0 2.2.4l1.2-.5a2 2 0 0 1 2.5.9l1 1.7c.6 1 .3 2.3-.7 2.9-2 1.1-4.5 1.1-6.5 0-2.5-1.3-4.8-3.6-6.1-6.1-1.1-2-1.1-4.5 0-6.5Z"/></svg>
|
||||
Telefoni ({{ allPhones.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'emails'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='emails' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M2.5 5A1.5 1.5 0 0 1 4 3.5h12A1.5 1.5 0 0 1 17.5 5v10A1.5 1.5 0 0 1 16 16.5H4A1.5 1.5 0 0 1 2.5 15V5Zm2.1.5 5.4 3.6a1 1 0 0 0 1.1 0l5.4-3.6V5H4.6Z"/></svg>
|
||||
E-pošta ({{ allEmails.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'bank'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='bank' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2 2 6v2h16V6l-8-4Zm-6 7h12v7H4V9Zm-1 8h14v1H3v-1Z"/></svg>
|
||||
TRR ({{ allBankAccounts.length }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<!-- Addresses -->
|
||||
<div v-if="activeTab==='addresses'">
|
||||
<div v-if="!allAddresses.length" class="text-gray-500 text-xs">Ni naslovov.</div>
|
||||
<div v-for="(a,idx) in allAddresses" :key="a.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ a.address }}</div>
|
||||
<div v-if="a.country" class="text-gray-600 text-xs">{{ a.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phones -->
|
||||
<div v-else-if="activeTab==='phones'">
|
||||
<div v-if="!allPhones.length" class="text-gray-500 text-xs">Ni telefonov.</div>
|
||||
<div v-for="(p,idx) in allPhones" :key="p.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ p.nu }} <span v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name" class="text-gray-500 text-xs">({{ p.type?.name || phoneTypes[p.type_id] }})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emails -->
|
||||
<div v-else-if="activeTab==='emails'">
|
||||
<div v-if="!allEmails.length" class="text-gray-500 text-xs">Ni e-poštnih naslovov.</div>
|
||||
<div v-for="(e,idx) in allEmails" :key="e.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ e.value }}<span v-if="e.label" class="text-gray-500 text-xs ml-1">({{ e.label }})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bank accounts -->
|
||||
<div v-else>
|
||||
<div v-if="!allBankAccounts.length" class="text-gray-500 text-xs">Ni TRR računov.</div>
|
||||
<div v-for="(b,idx) in allBankAccounts" :key="b.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ maskIban(b.iban) }}</div>
|
||||
<div v-if="b.bank_name" class="text-gray-600 text-xs">{{ b.bank_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user