Initial commit
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Frontend development
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.cache/
|
||||||
|
.vite/
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Frontend build
|
||||||
|
/static/
|
||||||
|
|
||||||
|
# Backend development
|
||||||
|
/lib/vendor/
|
||||||
|
coverage/
|
||||||
|
phpunit.xml.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.phpstan.cache
|
||||||
|
.phpactor/
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
9
composer.json
Normal file
9
composer.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "ktxm/people",
|
||||||
|
"type": "project",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"KTXM\\People\\": "lib/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/Module.php
Normal file
80
lib/Module.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXM\People;
|
||||||
|
|
||||||
|
use KTXF\Module\ModuleBrowserInterface;
|
||||||
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* People Module
|
||||||
|
*/
|
||||||
|
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
return 'people';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'People';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): string
|
||||||
|
{
|
||||||
|
return 'Ktrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'People management interface - provides address book selection and contact viewing/editing capabilities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function version(): string
|
||||||
|
{
|
||||||
|
return '0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'people' => [
|
||||||
|
'label' => 'Access People',
|
||||||
|
'description' => 'View and access the people module',
|
||||||
|
'group' => 'People Management'
|
||||||
|
],
|
||||||
|
'people.contacts.view' => [
|
||||||
|
'label' => 'View Contacts',
|
||||||
|
'description' => 'View contact details',
|
||||||
|
'group' => 'People Management'
|
||||||
|
],
|
||||||
|
'people.contacts.edit' => [
|
||||||
|
'label' => 'Edit Contacts',
|
||||||
|
'description' => 'Edit contact information',
|
||||||
|
'group' => 'People Management'
|
||||||
|
],
|
||||||
|
'people.*' => [
|
||||||
|
'label' => 'Full People Management',
|
||||||
|
'description' => 'All people management operations',
|
||||||
|
'group' => 'People Management'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerBI(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'handle' => $this->handle(),
|
||||||
|
'namespace' => 'People',
|
||||||
|
'version' => $this->version(),
|
||||||
|
'label' => $this->label(),
|
||||||
|
'author' => $this->author(),
|
||||||
|
'description' => $this->description(),
|
||||||
|
'boot' => 'static/module.mjs',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
1527
package-lock.json
generated
Normal file
1527
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@ktrix/people",
|
||||||
|
"description": "Ktrix People Module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"author": "Sebastian Krupinski",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --mode production --config vite.config.ts",
|
||||||
|
"dev": "vite build --mode development --config vite.config.ts",
|
||||||
|
"watch": "vite build --mode development --watch --config vite.config.ts",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^2.3.1",
|
||||||
|
"vue": "^3.5.18",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vuetify": "^3.10.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vue-tsc": "^3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
305
src/components/CollectionEditor.vue
Normal file
305
src/components/CollectionEditor.vue
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useServicesStore } from '@PeopleManager/stores/servicesStore'
|
||||||
|
import { CollectionObject } from '@PeopleManager/models/collection'
|
||||||
|
import { ServiceObject } from '@PeopleManager/models/service'
|
||||||
|
|
||||||
|
// Store
|
||||||
|
const servicesStore = useServicesStore()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
collection: CollectionObject | null
|
||||||
|
mode: 'create' | 'edit'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
'save': [collection: CollectionObject, service: ServiceObject]
|
||||||
|
'delete': [collection: CollectionObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// State
|
||||||
|
const services = ref<ServiceObject[]>([])
|
||||||
|
const editingCollection = ref<CollectionObject | null>(null)
|
||||||
|
const editingCollectionService = ref<ServiceObject | null>(null)
|
||||||
|
const editingCollectionValidated = ref(false)
|
||||||
|
const colorMenuOpen = ref(false)
|
||||||
|
const colorOptions = [
|
||||||
|
'#FF6B6B', '#FF8E4D', '#FFB347', '#FFD166', '#E5FF7F', '#B5E86D',
|
||||||
|
'#7CD992', '#46CBB7', '#2ECCCB', '#27B1E6', '#2196F3', '#2E6FE3',
|
||||||
|
'#3A4CC9', '#5A3FC0', '#7F3FBF', '#B339AE', '#DE3A9B', '#FF6FB7'
|
||||||
|
]
|
||||||
|
|
||||||
|
const COLORS_PER_ROW = 6
|
||||||
|
const colorRows = computed(() => {
|
||||||
|
const rows: string[][] = []
|
||||||
|
for (let i = 0; i < colorOptions.length; i += COLORS_PER_ROW) {
|
||||||
|
rows.push(colorOptions.slice(i, i + COLORS_PER_ROW))
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const dialogOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const onOpen = async () => {
|
||||||
|
if (services.value.length === 0) {
|
||||||
|
services.value = await servicesStore.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.collection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the collection to avoid mutating the original
|
||||||
|
editingCollection.value = props.collection.clone()
|
||||||
|
|
||||||
|
if (props.collection.id !== null) {
|
||||||
|
// Edit mode - find the service
|
||||||
|
editingCollectionService.value = services.value.find(s =>
|
||||||
|
s.provider === props.collection!.provider && s.id === props.collection!.service
|
||||||
|
) || null
|
||||||
|
} else {
|
||||||
|
// Create mode - use first service that can create
|
||||||
|
editingCollectionService.value = services.value.filter(s => s.capabilities?.CollectionCreate)[0] || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
if (!editingCollection.value || !editingCollectionService.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('save', editingCollection.value, editingCollectionService.value)
|
||||||
|
dialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
if (!editingCollection.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('delete', editingCollection.value)
|
||||||
|
dialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onColorSelect = (color: string | null, closeMenu = true) => {
|
||||||
|
if (!editingCollection.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editingCollection.value.color = color
|
||||||
|
if (closeMenu) {
|
||||||
|
colorMenuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCustomColorChange = (color: string) => {
|
||||||
|
if (!color) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onColorSelect(color, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for dialog opening
|
||||||
|
watch(() => props.modelValue, async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await onOpen()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialogOpen"
|
||||||
|
max-width="500"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon icon="mdi-book-plus" class="mr-2" />
|
||||||
|
{{ mode === 'create' ? 'Create New Address Book' : 'Edit Address Book' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text v-if="editingCollection" class="pt-4">
|
||||||
|
<v-form v-model="editingCollectionValidated" ref="collectionEditor">
|
||||||
|
<v-combobox v-show="mode === 'create' && services.length > 1"
|
||||||
|
v-model="editingCollectionService"
|
||||||
|
label="Service"
|
||||||
|
:items="services.filter(s => s.capabilities?.CollectionCreate)"
|
||||||
|
item-title="label"
|
||||||
|
item-value="id"
|
||||||
|
required
|
||||||
|
:rules="[(v: ServiceObject) => !!v || 'Service is required']"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mb-4"><strong>Service </strong> {{ editingCollection.service }}</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="editingCollection.label"
|
||||||
|
label="Label"
|
||||||
|
required
|
||||||
|
:rules="[(v: string) => !!v || 'Label is required']"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-menu
|
||||||
|
v-model="colorMenuOpen"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
location="bottom end"
|
||||||
|
offset="8"
|
||||||
|
>
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="menuProps"
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
:style="{ color: editingCollection.color || 'var(--v-theme-on-surface)' }"
|
||||||
|
aria-label="Select book color"
|
||||||
|
title="Select color"
|
||||||
|
>
|
||||||
|
<v-icon icon="mdi-palette" />
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card class="pa-2 color-menu" elevation="4">
|
||||||
|
<div class="color-menu-header">
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="color-menu-header__close"
|
||||||
|
aria-label="Close"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<v-icon icon="mdi-close" />
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="color-menu-body__presets">
|
||||||
|
<div
|
||||||
|
v-for="(rowColors, rowIndex) in colorRows"
|
||||||
|
:key="`color-row-${rowIndex}`"
|
||||||
|
class="color-menu-body__row"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
v-for="color in rowColors"
|
||||||
|
:key="color"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="color-menu-body__presets--swatch"
|
||||||
|
:class="{ 'color-menu-body__presets--swatch--active': editingCollection.color === color }"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
@click="onColorSelect(color)">
|
||||||
|
<v-icon
|
||||||
|
v-if="editingCollection.color === color"
|
||||||
|
icon="mdi-check"
|
||||||
|
size="x-small"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="color-menu-body__picker">
|
||||||
|
<v-color-picker
|
||||||
|
v-model="editingCollection.color"
|
||||||
|
mode="hex"
|
||||||
|
hide-canvas
|
||||||
|
width="100%"
|
||||||
|
elevation="0"
|
||||||
|
@update:model-value="onCustomColorChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="editingCollection.description"
|
||||||
|
label="Description"
|
||||||
|
rows="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col v-if="mode === 'edit'" cols="6">
|
||||||
|
<v-switch
|
||||||
|
v-model="editingCollection.enabled"
|
||||||
|
label="Enabled"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="justify-space-between align-center">
|
||||||
|
<div>
|
||||||
|
<v-btn
|
||||||
|
v-if="mode === 'edit' && editingCollectionService?.capabilities?.CollectionDestroy"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="onDelete"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
@click="dialogOpen = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
:disabled="!editingCollectionValidated"
|
||||||
|
@click="onSave"
|
||||||
|
>
|
||||||
|
{{ mode === 'create' ? 'Create' : 'Save' }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-menu {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-menu-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-menu-header__close {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-menu-body__presets {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-menu-body__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-menu-body__picker {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
src/components/CollectionList.vue
Normal file
127
src/components/CollectionList.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore'
|
||||||
|
import { CollectionObject } from '@PeopleManager/models/collection';
|
||||||
|
|
||||||
|
// Store
|
||||||
|
const collectionsStore = useCollectionsStore()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedCollection?: CollectionObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'select': [collection: CollectionObject]
|
||||||
|
'edit': [collection: CollectionObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// State
|
||||||
|
const loading = ref(false)
|
||||||
|
const collections = ref<CollectionObject[]>([])
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
collections.value = await collectionsStore.list()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[People] - Failed to load collections:', error)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const onCollectionSelect = (collection: CollectionObject) => {
|
||||||
|
console.log('[People] - Collection selected', collection)
|
||||||
|
emit('select', collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCollectionEdit = (collection: CollectionObject) => {
|
||||||
|
emit('edit', collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose refresh method
|
||||||
|
defineExpose({
|
||||||
|
async refresh() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
collections.value = await collectionsStore.list()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[People] - Failed to load collections:', error)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="collection-selector">
|
||||||
|
<div class="collection-selector-header">
|
||||||
|
<div class="d-flex align-center mb-3">
|
||||||
|
<v-icon icon="mdi-book-multiple" size="small" class="mr-2" />
|
||||||
|
<span class="text-subtitle-2 font-weight-bold">Address Books</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<div class="collection-selector-content">
|
||||||
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||||
|
|
||||||
|
<v-list v-else density="compact" nav class="pa-0">
|
||||||
|
<v-list-item
|
||||||
|
v-for="collection in collections"
|
||||||
|
:key="collection.id"
|
||||||
|
:value="collection.id"
|
||||||
|
:active="selectedCollection?.id === collection.id"
|
||||||
|
@click="onCollectionSelect(collection)"
|
||||||
|
rounded="lg"
|
||||||
|
class="mb-1"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon :color="collection.color || 'primary'" icon="mdi-book-outline" size="small" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title>{{ collection.label }}</v-list-item-title>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
@click.stop="onCollectionEdit(collection)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-alert v-if="!loading && collections.length === 0" type="info" variant="tonal" density="compact" class="mt-2">
|
||||||
|
No address books found
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.collection-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-selector-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-selector-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
400
src/components/PersonEditor.vue
Normal file
400
src/components/PersonEditor.vue
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { EntityObject } from '@PeopleManager/models/entity'
|
||||||
|
import PersonEditorNames from './editors/PersonEditorNames.vue'
|
||||||
|
import PersonEditorTitles from './editors/PersonEditorTitles.vue'
|
||||||
|
import PersonEditorAnniversaries from './editors/PersonEditorAnniversaries.vue'
|
||||||
|
import PersonEditorPhysicalLocations from './editors/PersonEditorPhysicalLocations.vue'
|
||||||
|
import PersonEditorPhones from './editors/PersonEditorPhones.vue'
|
||||||
|
import PersonEditorEmails from './editors/PersonEditorEmails.vue'
|
||||||
|
import PersonEditorOrganizations from './editors/PersonEditorOrganizations.vue'
|
||||||
|
import PersonEditorNotes from './editors/PersonEditorNotes.vue'
|
||||||
|
import PersonEditorLanguages from './editors/PersonEditorLanguages.vue'
|
||||||
|
import PersonEditorMedia from './editors/PersonEditorMedia.vue'
|
||||||
|
import PersonEditorLabel from './editors/PersonEditorLabel.vue'
|
||||||
|
import PersonEditorVirtualLocations from './editors/PersonEditorVirtualLocations.vue'
|
||||||
|
import PersonEditorMembers from './editors/PersonEditorMembers.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
selectedEntity?: EntityObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'edit': []
|
||||||
|
'cancel': []
|
||||||
|
'close': []
|
||||||
|
'save': [entity: EntityObject]
|
||||||
|
'delete': [entity: EntityObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// State
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const mode = computed(() => props.mode)
|
||||||
|
const entity = computed(() => props.selectedEntity || null)
|
||||||
|
const entityObject = computed(() => entity.value?.data ?? null)
|
||||||
|
const entityFresh = computed(() => entity.value?.id === null || entity.value?.id === undefined)
|
||||||
|
const entityType = computed(() => entityObject.value?.type || 'individual')
|
||||||
|
|
||||||
|
// Determine which sections to show based on entity type
|
||||||
|
const showNames = computed(() => entityType.value === 'individual')
|
||||||
|
const showTitles = computed(() => entityType.value === 'individual')
|
||||||
|
const showAnniversaries = computed(() => entityType.value === 'individual')
|
||||||
|
const showOrganizations = computed(() => entityType.value === 'individual')
|
||||||
|
const showLanguages = computed(() => entityType.value === 'individual')
|
||||||
|
const showMembers = computed(() => entityType.value === 'group')
|
||||||
|
|
||||||
|
// Sections shown for all types
|
||||||
|
const showLabel = computed(() => true)
|
||||||
|
const showPhysicalLocations = computed(() => entityType.value !== 'group')
|
||||||
|
const showPhones = computed(() => entityType.value !== 'group')
|
||||||
|
const showEmails = computed(() => entityType.value !== 'group')
|
||||||
|
const showVirtualLocations = computed(() => true)
|
||||||
|
const showMedia = computed(() => true)
|
||||||
|
const showNotes = computed(() => true)
|
||||||
|
|
||||||
|
// UI helpers
|
||||||
|
const entityIcon = computed(() => {
|
||||||
|
switch (entityType.value) {
|
||||||
|
case 'organization':
|
||||||
|
return 'mdi-domain'
|
||||||
|
case 'group':
|
||||||
|
return 'mdi-account-group'
|
||||||
|
default:
|
||||||
|
return 'mdi-account'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const startEdit = () => {
|
||||||
|
emit('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEntity = async () => {
|
||||||
|
saving.value = true
|
||||||
|
emit('save', entity.value!)
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title management
|
||||||
|
const addTitle = () => {
|
||||||
|
(entityObject.value as any)?.addTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTitle = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeTitle(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anniversary management
|
||||||
|
const addAnniversary = () => {
|
||||||
|
(entityObject.value as any)?.addAnniversary()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAnniversary = (index: number) => {
|
||||||
|
(entityObject.value as any)?.removeAnniversary(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAnniversaryDate = (anniversary: any, date: string) => {
|
||||||
|
anniversary.when = date
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physical location management
|
||||||
|
const addPhysicalLocation = () => {
|
||||||
|
(entityObject.value as any)?.addPhysicalLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePhysicalLocation = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removePhysicalLocation(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone management
|
||||||
|
const addPhone = () => {
|
||||||
|
(entityObject.value as any)?.addPhone()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePhone = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removePhone(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email management
|
||||||
|
const addEmail = () => {
|
||||||
|
(entityObject.value as any)?.addEmail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeEmail = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeEmail(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization management
|
||||||
|
const addOrganization = () => {
|
||||||
|
(entityObject.value as any)?.addOrganization()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeOrganization = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeOrganization(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note management
|
||||||
|
const addNote = () => {
|
||||||
|
(entityObject.value as any)?.addNote()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeNote = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeNote(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language management
|
||||||
|
const addLanguage = () => {
|
||||||
|
(entityObject.value as any)?.addLanguage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLanguage = (index: number) => {
|
||||||
|
(entityObject.value as any)?.removeLanguage(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media management
|
||||||
|
const addMedia = () => {
|
||||||
|
(entityObject.value as any)?.addMedia()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMedia = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeMedia(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member management (for groups)
|
||||||
|
const addMember = () => {
|
||||||
|
(entityObject.value as any)?.addMember()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMember = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeMember(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual location management
|
||||||
|
const addVirtualLocation = () => {
|
||||||
|
(entityObject.value as any)?.addVirtualLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeVirtualLocation = (key: string) => {
|
||||||
|
(entityObject.value as any)?.removeVirtualLocation(key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="person-editor-card">
|
||||||
|
<div class="people-editor-header d-flex justify-space-between pa-4">
|
||||||
|
<div>
|
||||||
|
<v-icon :icon="entityIcon" class="mr-2" />
|
||||||
|
{{ entity?.data?.label || 'Nothing Selected' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && entity">
|
||||||
|
<v-btn
|
||||||
|
v-if="mode === 'view'"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="startEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="!entityFresh"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="emit('delete', entity!)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="people-editor-fields">
|
||||||
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||||
|
|
||||||
|
<div v-if="!entity" class="text-center pa-8">
|
||||||
|
<v-icon icon="mdi-account-search" size="64" color="grey" class="mb-4" />
|
||||||
|
<p class="text-h6 text-grey">Select an individual, organization or group to view details</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="pa-4">
|
||||||
|
<PersonEditorLabel
|
||||||
|
v-if="showLabel"
|
||||||
|
:mode="mode"
|
||||||
|
:label="entity.data!.label"
|
||||||
|
@update:label="entity.data!.label = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorNames
|
||||||
|
v-if="showNames"
|
||||||
|
:mode="mode"
|
||||||
|
:names="entity.data!.names"
|
||||||
|
@update:names="entity.data!.names = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorTitles
|
||||||
|
v-if="showTitles"
|
||||||
|
:titles="(entity.data as any)!.titles || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-title="addTitle"
|
||||||
|
@remove-title="removeTitle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorAnniversaries
|
||||||
|
v-if="showAnniversaries"
|
||||||
|
:anniversaries="(entity.data as any)!.anniversaries || []"
|
||||||
|
:mode="mode"
|
||||||
|
@add-anniversary="addAnniversary"
|
||||||
|
@remove-anniversary="removeAnniversary"
|
||||||
|
@update-anniversary-date="updateAnniversaryDate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorPhysicalLocations
|
||||||
|
v-if="showPhysicalLocations"
|
||||||
|
:physical-locations="(entity.data as any)!.physicalLocations || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-physical-location="addPhysicalLocation"
|
||||||
|
@remove-physical-location="removePhysicalLocation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorPhones
|
||||||
|
v-if="showPhones"
|
||||||
|
:phones="(entity.data as any)!.phones || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-phone="addPhone"
|
||||||
|
@remove-phone="removePhone"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorEmails
|
||||||
|
v-if="showEmails"
|
||||||
|
:emails="(entity.data as any)!.emails || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-email="addEmail"
|
||||||
|
@remove-email="removeEmail"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorVirtualLocations
|
||||||
|
v-if="showVirtualLocations"
|
||||||
|
:virtual-locations="(entity.data as any)!.virtualLocations || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-virtual-location="addVirtualLocation"
|
||||||
|
@remove-virtual-location="removeVirtualLocation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorOrganizations
|
||||||
|
v-if="showOrganizations"
|
||||||
|
:organizations="(entity.data as any)!.organizations || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-organization="addOrganization"
|
||||||
|
@remove-organization="removeOrganization"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorMembers
|
||||||
|
v-if="showMembers"
|
||||||
|
:members="(entity.data as any)!.members || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-member="addMember"
|
||||||
|
@remove-member="removeMember"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorNotes
|
||||||
|
v-if="showNotes"
|
||||||
|
:notes="entity.data!.notes || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-note="addNote"
|
||||||
|
@remove-note="removeNote"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorLanguages
|
||||||
|
v-if="showLanguages"
|
||||||
|
:languages="(entity.data as any)!.languages || []"
|
||||||
|
:mode="mode"
|
||||||
|
@add-language="addLanguage"
|
||||||
|
@remove-language="removeLanguage"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonEditorMedia
|
||||||
|
v-if="showMedia"
|
||||||
|
:media="entity.data!.media || {}"
|
||||||
|
:mode="mode"
|
||||||
|
@add-media="addMedia"
|
||||||
|
@remove-media="removeMedia"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="people-editor-footer d-flex text-end justify-space-between pa-4">
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
variant="text"
|
||||||
|
@click="cancelEdit"
|
||||||
|
:disabled="saving">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
@click="saveEntity"
|
||||||
|
:loading="saving">
|
||||||
|
Save
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-editor-header {
|
||||||
|
background: transparent;
|
||||||
|
min-height: 72px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-editor-fields {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: transparent;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-editor-footer {
|
||||||
|
background: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-editor-card :deep(.v-field) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-editor-card :deep(.v-input) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-editor-card :deep(.v-card) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
265
src/components/PersonList.vue
Normal file
265
src/components/PersonList.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useEntitiesStore } from '@PeopleManager/stores/entitiesStore'
|
||||||
|
import type { CollectionObject } from '@PeopleManager/models/collection'
|
||||||
|
import type { EntityObject } from '@PeopleManager/models/entity'
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
const entitiesStore = useEntitiesStore()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedCollection?: CollectionObject | null
|
||||||
|
selectedEntity?: EntityObject | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'select': [entity: EntityObject]
|
||||||
|
'fresh': [type: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// State
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const collectionEntities = ref<EntityObject[]>([])
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const filteredEntities = computed(() => {
|
||||||
|
if (!searchQuery.value) {
|
||||||
|
return collectionEntities.value
|
||||||
|
}
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
return collectionEntities.value.filter(entity => {
|
||||||
|
const name = `${entity.data?.names.family || ''} ${entity.data?.names.given || ''}`.toLowerCase()
|
||||||
|
const email = (entity.data?.emails?.[0]?.address || '').toLowerCase()
|
||||||
|
return name.includes(query) || email.includes(query)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watchers
|
||||||
|
watch(() => props.selectedCollection, async (newCollection) => {
|
||||||
|
if (newCollection) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
collectionEntities.value = await entitiesStore.list(
|
||||||
|
newCollection.provider,
|
||||||
|
newCollection.service,
|
||||||
|
newCollection.id
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[People] - Failed to load contacts:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collectionEntities.value = []
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
function entityType(entity: EntityObject): string {
|
||||||
|
return entity.data?.type || 'individual'
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityIcon(entity: EntityObject): string {
|
||||||
|
const type = entityType(entity)
|
||||||
|
switch (type) {
|
||||||
|
case 'organization':
|
||||||
|
return 'mdi-domain'
|
||||||
|
case 'group':
|
||||||
|
return 'mdi-account-group'
|
||||||
|
default:
|
||||||
|
return 'mdi-account'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityPhoto(entity: EntityObject): string | null {
|
||||||
|
// TODO: Implement photo retrieval from entity data
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityInitials(entity: EntityObject): string {
|
||||||
|
const type = entityType(entity)
|
||||||
|
|
||||||
|
if (type === 'organization') {
|
||||||
|
const name = (entity.data as any)?.names?.full || entity.data?.label || ''
|
||||||
|
return name.substring(0, 2).toUpperCase() || 'OR'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'group') {
|
||||||
|
const name = (entity.data as any)?.names?.full || entity.data?.label || ''
|
||||||
|
return name.substring(0, 2).toUpperCase() || 'GR'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual
|
||||||
|
const given = (entity.data as any)?.names?.given
|
||||||
|
const family = (entity.data as any)?.names?.family
|
||||||
|
const initials = `${given?.[0] || ''}${family?.[0] || ''}`.toUpperCase()
|
||||||
|
return initials || '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityLabel(entity: EntityObject): string {
|
||||||
|
const type = entityType(entity)
|
||||||
|
|
||||||
|
if (type === 'organization' || type === 'group') {
|
||||||
|
return entity.data?.label || (entity.data as any)?.names?.full || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual
|
||||||
|
return entity.data?.label || `${(entity.data as any)?.names?.given || ''} ${(entity.data as any)?.names?.family || ''}`.trim() || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityEmail(entity: EntityObject): string | null {
|
||||||
|
const emails = (entity.data as any)?.emails
|
||||||
|
if (!emails) return null
|
||||||
|
const emailEntries = Object.values(emails)
|
||||||
|
if (emailEntries.length === 0) return null
|
||||||
|
// Sort by priority (assuming lower number = higher priority)
|
||||||
|
emailEntries.sort((a: any, b: any) => (a.priority || 999) - (b.priority || 999))
|
||||||
|
return (emailEntries[0] as any).address || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityOrganization(entity: EntityObject): string | null {
|
||||||
|
const organizations = (entity.data as any)?.organizations
|
||||||
|
if (!organizations || Object.keys(organizations).length === 0) return null
|
||||||
|
const orgEntries = Object.values(organizations)
|
||||||
|
return (orgEntries[0] as any).Label || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityMemberCount(entity: EntityObject): number | null {
|
||||||
|
const type = entityType(entity)
|
||||||
|
if (type !== 'group') return null
|
||||||
|
const members = (entity.data as any)?.members
|
||||||
|
if (!members) return 0
|
||||||
|
return Object.keys(members).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityAvatarColor(entity: EntityObject): string {
|
||||||
|
const type = entityType(entity)
|
||||||
|
switch (type) {
|
||||||
|
case 'organization':
|
||||||
|
return 'blue-darken-1'
|
||||||
|
case 'group':
|
||||||
|
return 'green-darken-1'
|
||||||
|
default:
|
||||||
|
return 'primary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="person-list-container">
|
||||||
|
<div class="person-list-header pa-4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
label="Search contacts"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pa-0 person-list-content">
|
||||||
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||||
|
|
||||||
|
<div v-else-if="!selectedCollection" class="text-center pa-8">
|
||||||
|
<v-icon icon="mdi-book-alert" size="64" color="grey" class="mb-4" />
|
||||||
|
<p class="text-body-1 text-grey">Select an address book to view contacts</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredEntities.length === 0" class="text-center pa-8">
|
||||||
|
<v-icon icon="mdi-account-search" size="64" color="grey" class="mb-4" />
|
||||||
|
<p class="text-body-1 text-grey">No Individuals, Organizations or Groups found</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list v-else density="compact" nav>
|
||||||
|
<v-list-item
|
||||||
|
v-for="entity in filteredEntities"
|
||||||
|
:key="entity.id"
|
||||||
|
:value="entity.id"
|
||||||
|
@click="$emit('select', entity)">
|
||||||
|
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar :color="entityAvatarColor(entity)" size="40">
|
||||||
|
<v-img v-if="entityPhoto(entity)" :src="entityPhoto(entity)!" />
|
||||||
|
<v-icon v-else-if="entityType(entity) !== 'individual'" :icon="entityIcon(entity)" size="small" />
|
||||||
|
<span v-else class="text-subtitle-2">{{ entityInitials(entity) }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon :icon="entityIcon(entity)" size="x-small" class="mr-1" />
|
||||||
|
{{ entityLabel(entity) }}
|
||||||
|
</v-list-item-title>
|
||||||
|
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<!-- For individuals: show email and organization -->
|
||||||
|
<template v-if="entityType(entity) === 'individual'">
|
||||||
|
<div v-if="entityEmail(entity)">
|
||||||
|
<v-icon icon="mdi-email" size="x-small" class="mr-1" />
|
||||||
|
{{ entityEmail(entity) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="entityOrganization(entity)">
|
||||||
|
<v-icon icon="mdi-domain" size="x-small" class="mr-1" />
|
||||||
|
{{ entityOrganization(entity) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- For organizations: show email -->
|
||||||
|
<template v-else-if="entityType(entity) === 'organization'">
|
||||||
|
<div v-if="entityEmail(entity)">
|
||||||
|
<v-icon icon="mdi-email" size="x-small" class="mr-1" />
|
||||||
|
{{ entityEmail(entity) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- For groups: show member count -->
|
||||||
|
<template v-else-if="entityType(entity) === 'group'">
|
||||||
|
<div>
|
||||||
|
<v-icon icon="mdi-account-multiple" size="x-small" class="mr-1" />
|
||||||
|
{{ entityMemberCount(entity) }} member{{ entityMemberCount(entity) !== 1 ? 's' : '' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-list-header {
|
||||||
|
min-height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-list-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-list-container :deep(.v-list) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-list-container :deep(.v-field) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
src/components/editors/PersonEditorAnniversaries.vue
Normal file
115
src/components/editors/PersonEditorAnniversaries.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
anniversaries: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-anniversary': []
|
||||||
|
'remove-anniversary': [index: number]
|
||||||
|
'update-anniversary-date': [anniversary: any, date: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard()
|
||||||
|
|
||||||
|
const updateAnniversaryDate = (anniversary: any, date: string) => {
|
||||||
|
emit('update-anniversary-date', anniversary, date)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="(anniversaries && anniversaries.length > 0) || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Anniversaries</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-anniversary')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Anniversary
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(anniversary, index) in anniversaries" :key="index" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-cake-variant" size="small" class="mr-2" />
|
||||||
|
<strong>{{ anniversary.type || 'Unknown' }} Anniversary</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === index ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === index ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(anniversary.when?.date || '', index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Date: {{ anniversary.when?.date || 'Not set' }} |
|
||||||
|
Location: {{ anniversary.location || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(anniversary, index) in anniversaries" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Anniversary</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-anniversary', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="anniversary.type"
|
||||||
|
:items="['birth', 'death', 'nuptial']"
|
||||||
|
label="Type"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="anniversary.location"
|
||||||
|
label="Location"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="anniversary.when?.date || ''"
|
||||||
|
@update:model-value="updateAnniversaryDate(anniversary, $event)"
|
||||||
|
label="Date"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
108
src/components/editors/PersonEditorEmails.vue
Normal file
108
src/components/editors/PersonEditorEmails.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
emails: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-email': []
|
||||||
|
'remove-email': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(emails || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Emails</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-email')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Email
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(email, key) in emails" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-email" size="small" class="mr-2" />
|
||||||
|
<strong>{{ email.address || 'No address' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(email.address || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Context: {{ email.context || 'Not set' }} |
|
||||||
|
Priority: {{ email.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(email, index) in emails" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Email</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-email', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="email.address"
|
||||||
|
label="Address"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="email.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="email.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/components/editors/PersonEditorLabel.vue
Normal file
61
src/components/editors/PersonEditorLabel.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
label?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:label': [label: string | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
|
||||||
|
const labelValue = computed({
|
||||||
|
get: () => props.label ?? '',
|
||||||
|
set: (value) => emit('update:label', value || null)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="person-editor-section">
|
||||||
|
<div class="text-subtitle-1 mb-2">Label</div>
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'" class="mb-4">
|
||||||
|
<div class="d-flex align-center justify-space-between pa-2">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-label" size="small" class="mr-2" />
|
||||||
|
<span>{{ labelValue || 'Not set' }}</span>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
v-if="labelValue"
|
||||||
|
:icon="copiedKey === 'label' ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === 'label' ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(labelValue, 'label')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<v-text-field
|
||||||
|
v-model="labelValue"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
src/components/editors/PersonEditorLanguages.vue
Normal file
117
src/components/editors/PersonEditorLanguages.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
languages: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-language': []
|
||||||
|
'remove-language': [index: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="(languages || []).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Languages</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-language')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Language
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(lang, index) in languages" :key="index" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-translate" size="small" class="mr-2" />
|
||||||
|
<strong>{{ lang.Data || 'Unknown Language' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === index ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === index ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(lang.Data || '', index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
ID: {{ lang.Id || 'Not set' }} |
|
||||||
|
Context: {{ lang.Context || 'Not set' }} |
|
||||||
|
Priority: {{ lang.Priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(lang, index) in languages" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Language</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-language', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="lang.Data"
|
||||||
|
label="Language"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="lang.Id"
|
||||||
|
label="Language ID"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="lang.Context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="lang.Priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
148
src/components/editors/PersonEditorMedia.vue
Normal file
148
src/components/editors/PersonEditorMedia.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
media: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-media': []
|
||||||
|
'remove-media': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(media || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Media</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-media')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Media
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(item, key) in media" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-image" size="small" class="mr-2" />
|
||||||
|
<strong>{{ item.label || item.uri || 'Unnamed Media' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(item.uri || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Type: {{ item.type || 'Not set' }} |
|
||||||
|
Kind: {{ item.kind || 'Not set' }} |
|
||||||
|
Contexts: {{ (item.contexts || []).join(', ') || 'None' }} |
|
||||||
|
Pref: {{ item.pref || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(item, index) in media" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Media</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-media', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.uri"
|
||||||
|
label="URI"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="item.type"
|
||||||
|
:items="['photo', 'video', 'audio', 'document']"
|
||||||
|
label="Type"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="item.kind"
|
||||||
|
:items="['photo', 'sound', 'logo']"
|
||||||
|
label="Kind"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.mediaType"
|
||||||
|
label="Media Type"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="item.contexts"
|
||||||
|
label="Contexts (comma-separated)"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hint="Enter contexts separated by commas"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="item.pref"
|
||||||
|
label="Preference"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
src/components/editors/PersonEditorMembers.vue
Normal file
117
src/components/editors/PersonEditorMembers.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
members: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-member': []
|
||||||
|
'remove-member': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(members || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Members</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-member')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Member
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(member, key) in members" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-account" size="small" class="mr-2" />
|
||||||
|
<strong>Entity ID: {{ member.entityId || 'Not set' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(member.entityId || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Role: {{ member.role || 'Not set' }} |
|
||||||
|
Context: {{ member.context || 'Not set' }} |
|
||||||
|
Priority: {{ member.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(member, index) in members" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Member</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-member', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="member.entityId"
|
||||||
|
label="Entity ID"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="member.role"
|
||||||
|
label="Role"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="member.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="member.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
139
src/components/editors/PersonEditorNames.vue
Normal file
139
src/components/editors/PersonEditorNames.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
names: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:names': [names: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
|
||||||
|
const names = computed({
|
||||||
|
get: () => props.names,
|
||||||
|
set: (value) => emit('update:names', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatName = (names: any) => {
|
||||||
|
if (!names) return 'Not set'
|
||||||
|
const parts = []
|
||||||
|
if (names.prefix) parts.push(names.prefix)
|
||||||
|
if (names.given) parts.push(names.given)
|
||||||
|
if (names.additional) parts.push(names.additional)
|
||||||
|
if (names.family) parts.push(names.family)
|
||||||
|
if (names.suffix) parts.push(names.suffix)
|
||||||
|
return parts.join(' ') || 'Not set'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="person-editor-section">
|
||||||
|
<div class="text-subtitle-1 mb-2">Name</div>
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'" class="mb-4">
|
||||||
|
<div class="d-flex align-center justify-space-between pa-2">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-account" size="small" class="mr-2" />
|
||||||
|
<span>{{ formatName(names) }}</span>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
v-if="formatName(names) !== 'Not set'"
|
||||||
|
:icon="copiedKey === 'name' ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === 'name' ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(formatName(names), 'name')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.given"
|
||||||
|
label="Given Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.family"
|
||||||
|
label="Family Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.additional"
|
||||||
|
label="Additional Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.prefix"
|
||||||
|
label="Prefix"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.suffix"
|
||||||
|
label="Suffix"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<div class="text-subtitle-1 mb-2 mt-4">Phonetic Name</div>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.phoneticGiven"
|
||||||
|
label="Phonetic Given Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.phoneticAdditional"
|
||||||
|
label="Phonetic Additional Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="names.phoneticFamily"
|
||||||
|
label="Phonetic Family Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
src/components/editors/PersonEditorNotes.vue
Normal file
160
src/components/editors/PersonEditorNotes.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
notes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-note': []
|
||||||
|
'remove-note': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(notes || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Notes</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-note')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Note
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(note, key) in notes" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-note-text" size="small" class="mr-2" />
|
||||||
|
<strong>{{ note.content || 'Empty note' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(note.content || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Date: {{ note.date?.date || 'Not set' }} |
|
||||||
|
Author: {{ note.authorName || note.authorUri || 'Unknown' }} |
|
||||||
|
Context: {{ note.context || 'Not set' }} |
|
||||||
|
Priority: {{ note.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(note, index) in notes" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Note</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-note', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="note.content"
|
||||||
|
label="Content"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="note.authorName"
|
||||||
|
label="Author Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="note.authorUri"
|
||||||
|
label="Author URI"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="note.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="note.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="note.date?.date || ''"
|
||||||
|
@update:model-value="updateNoteDate(note, $event)"
|
||||||
|
label="Date"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
updateNoteDate(note: any, date: string) {
|
||||||
|
if (!note.date) {
|
||||||
|
note.date = {
|
||||||
|
date: '',
|
||||||
|
timezone_type: 3,
|
||||||
|
timezone: 'UTC'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
note.date.date = date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
131
src/components/editors/PersonEditorOrganizations.vue
Normal file
131
src/components/editors/PersonEditorOrganizations.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
organizations: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-organization': []
|
||||||
|
'remove-organization': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="(organizations || []).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Organizations</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-organization')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Organization
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(org, key) in organizations" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-domain" size="small" class="mr-2" />
|
||||||
|
<strong>{{ org.Label || 'Unnamed Organization' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(org.Label || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Units: {{ (org.Units || []).join(', ') || 'None' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Context: {{ org.context || 'Not set' }} |
|
||||||
|
Priority: {{ org.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(org, index) in organizations" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Organization</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-organization', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="org.Label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="org.sortName"
|
||||||
|
label="Sort Name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="org.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="org.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="org.Units"
|
||||||
|
label="Units (comma-separated)"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hint="Enter units separated by commas"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
src/components/editors/PersonEditorPhones.vue
Normal file
117
src/components/editors/PersonEditorPhones.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
phones: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-phone': []
|
||||||
|
'remove-phone': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(phones || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Phones</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-phone')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Phone
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(phone, key) in phones" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-phone" size="small" class="mr-2" />
|
||||||
|
<strong>{{ phone.number || 'No number' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(phone.number || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Label: {{ phone.label || 'Not set' }} |
|
||||||
|
Context: {{ phone.context || 'Not set' }} |
|
||||||
|
Priority: {{ phone.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(phone, index) in phones" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Phone</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-phone', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="phone.number"
|
||||||
|
label="Number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="phone.label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="phone.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="phone.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
206
src/components/editors/PersonEditorPhysicalLocations.vue
Normal file
206
src/components/editors/PersonEditorPhysicalLocations.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
physicalLocations: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-physical-location': []
|
||||||
|
'remove-physical-location': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
|
||||||
|
const formatAddress = (location: any) => {
|
||||||
|
if (!location) return 'Not set'
|
||||||
|
const parts = []
|
||||||
|
if (location.street) parts.push(location.street)
|
||||||
|
if (location.locality) parts.push(location.locality)
|
||||||
|
if (location.region) parts.push(location.region)
|
||||||
|
if (location.code) parts.push(location.code)
|
||||||
|
if (location.country) parts.push(location.country)
|
||||||
|
return parts.join(', ') || 'Not set'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(physicalLocations || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Physical Locations</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-physical-location')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Location
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(location, key) in physicalLocations" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-map-marker" size="small" class="mr-2" />
|
||||||
|
<strong>{{ location.label || `Location ${parseInt(key.split('_')[1]) || key}` }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(formatAddress(location), key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
{{ formatAddress(location) }}
|
||||||
|
<span v-if="location.context"> | Context: {{ location.context }}</span>
|
||||||
|
<span v-if="location.priority"> | Priority: {{ location.priority }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(location, index) in physicalLocations" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>{{ location.label || `Location` }}</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-physical-location', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-subtitle-2 mb-2">Address</div>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.box"
|
||||||
|
label="Box"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.unit"
|
||||||
|
label="Unit"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.street"
|
||||||
|
label="Street"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.locality"
|
||||||
|
label="City/Locality"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.region"
|
||||||
|
label="State/Region"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.code"
|
||||||
|
label="Postal Code"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.country"
|
||||||
|
label="Country"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<div class="text-subtitle-2 mb-2">Additional Information</div>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.coordinates"
|
||||||
|
label="Coordinates"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.timeZone"
|
||||||
|
label="Time Zone"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="location.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="location.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/components/editors/PersonEditorTitles.vue
Normal file
130
src/components/editors/PersonEditorTitles.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
titles: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-title': []
|
||||||
|
'remove-title': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(titles || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Titles</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-title')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Title
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(title, key) in titles" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-badge-account" size="small" class="mr-2" />
|
||||||
|
<strong>{{ title.label || 'Untitled' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(title.label || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Kind: {{ title.kind || 'Not set' }} |
|
||||||
|
Relation: {{ title.relation || 'Not set' }} |
|
||||||
|
Context: {{ title.context || 'Not set' }} |
|
||||||
|
Priority: {{ title.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(title, key) in titles" :key="key" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Title</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-title', key)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="title.kind"
|
||||||
|
:items="['t', 'r']"
|
||||||
|
label="Kind"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="title.label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="title.relation"
|
||||||
|
label="Relation"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="title.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="title.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
src/components/editors/PersonEditorVirtualLocations.vue
Normal file
117
src/components/editors/PersonEditorVirtualLocations.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useClipboard } from '@KTXC/composables/useClipboard'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: 'edit' | 'view'
|
||||||
|
virtualLocations: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-virtual-location': []
|
||||||
|
'remove-virtual-location': [key: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { copiedKey, copyToClipboard } = useClipboard<string>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(virtualLocations || {}).length > 0 || mode === 'edit'" class="person-editor-section">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<div class="text-subtitle-1">Virtual Locations</div>
|
||||||
|
<v-btn v-if="mode === 'edit'"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
@click="$emit('add-virtual-location')">
|
||||||
|
<v-icon left>mdi-plus</v-icon>
|
||||||
|
Add Virtual Location
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only view -->
|
||||||
|
<div v-if="mode === 'view'">
|
||||||
|
<div v-for="(virtualLocation, key) in virtualLocations" :key="key" class="mb-2 pa-2 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-web" size="small" class="mr-2" />
|
||||||
|
<strong>{{ virtualLocation.location || 'No location' }}</strong>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
:icon="copiedKey === key ? 'mdi-check' : 'mdi-content-copy'"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
:color="copiedKey === key ? 'success' : undefined"
|
||||||
|
@click="copyToClipboard(virtualLocation.location || '', key as string)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Label: {{ virtualLocation.label || 'Not set' }} |
|
||||||
|
Context: {{ virtualLocation.context || 'Not set' }} |
|
||||||
|
Priority: {{ virtualLocation.priority || 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit view -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="(virtualLocation, index) in virtualLocations" :key="index" class="mb-4 pa-3 border rounded">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<strong>Virtual Location</strong>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('remove-virtual-location', index)">
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="virtualLocation.location"
|
||||||
|
label="Location (URL/Handle)"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="virtualLocation.label"
|
||||||
|
label="Label"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="virtualLocation.context"
|
||||||
|
label="Context"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="virtualLocation.priority"
|
||||||
|
label="Priority"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.person-editor-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/integrations.ts
Normal file
15
src/integrations.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||||
|
|
||||||
|
const integrations: ModuleIntegrations = {
|
||||||
|
app_menu: [
|
||||||
|
{
|
||||||
|
id: 'people',
|
||||||
|
label: 'People',
|
||||||
|
path: '/people',
|
||||||
|
icon: 'mdi-account-multiple',
|
||||||
|
priority: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default integrations;
|
||||||
7
src/main.ts
Normal file
7
src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import routes from '@/routes'
|
||||||
|
import integrations from '@/integrations'
|
||||||
|
|
||||||
|
// CSS filename is injected by the vite plugin at build time
|
||||||
|
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
|
export { routes, integrations }
|
||||||
392
src/pages/PeoplePage.vue
Normal file
392
src/pages/PeoplePage.vue
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { useModuleStore } from '@KTXC/stores/moduleStore'
|
||||||
|
import { useCollectionsStore } from '@PeopleManager/stores/collectionsStore'
|
||||||
|
import { useEntitiesStore } from '@PeopleManager/stores/entitiesStore'
|
||||||
|
import { ServiceObject } from '@PeopleManager/models/service';
|
||||||
|
import { CollectionObject } from '@PeopleManager/models/collection';
|
||||||
|
import { EntityObject } from '@PeopleManager/models/entity';
|
||||||
|
import CollectionList from '@/components/CollectionList.vue'
|
||||||
|
import CollectionEditor from '@/components/CollectionEditor.vue'
|
||||||
|
import PersonList from '@/components/PersonList.vue'
|
||||||
|
import PersonEditor from '@/components/PersonEditor.vue'
|
||||||
|
|
||||||
|
// Vuetify display
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
// View state
|
||||||
|
const sidebarVisible = ref(true)
|
||||||
|
|
||||||
|
// Check if people manager is available
|
||||||
|
const moduleStore = useModuleStore()
|
||||||
|
|
||||||
|
const isPeopleManagerAvailable = computed(() => {
|
||||||
|
return moduleStore.has('people_manager') || moduleStore.has('PeopleManager')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Local state for UI selections
|
||||||
|
const collectionsStore = useCollectionsStore()
|
||||||
|
const entitiesStore = useEntitiesStore()
|
||||||
|
const selectedCollection = ref<CollectionObject | null>(null)
|
||||||
|
const selectedEntity = ref<EntityObject | null>(null)
|
||||||
|
const entityEditorMode = ref<'edit' | 'view'>('view')
|
||||||
|
|
||||||
|
// Collection editor state
|
||||||
|
const showCollectionEditor = ref(false)
|
||||||
|
const editingCollection = ref<CollectionObject | null>(null)
|
||||||
|
const collectionEditorMode = ref<'create' | 'edit'>('create')
|
||||||
|
const CollectionListRef = ref<InstanceType<typeof CollectionList> | null>(null)
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleEditorEdit = () => {
|
||||||
|
console.log('[People] - Editor editing started')
|
||||||
|
entityEditorMode.value = 'edit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditorCancel = () => {
|
||||||
|
console.log('[People] - Editor editing cancelled')
|
||||||
|
entityEditorMode.value = 'view'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditorClose = () => {
|
||||||
|
console.log('[People] - Editor closed')
|
||||||
|
selectedEntity.value = null
|
||||||
|
entityEditorMode.value = 'view'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionSelect = (collection: CollectionObject) => {
|
||||||
|
console.log('[People] - Collection selected', collection)
|
||||||
|
selectedCollection.value = collection ?? null
|
||||||
|
selectedEntity.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionEdit = (collection: CollectionObject) => {
|
||||||
|
console.log('[People] - Collection edit', collection)
|
||||||
|
editingCollection.value = collection
|
||||||
|
collectionEditorMode.value = 'edit'
|
||||||
|
showCollectionEditor.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCollectionFresh = () => {
|
||||||
|
console.log('[People] - Collection new')
|
||||||
|
editingCollection.value = collectionsStore.fresh()
|
||||||
|
collectionEditorMode.value = 'create'
|
||||||
|
showCollectionEditor.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionSave = async (collection: CollectionObject, service: ServiceObject) => {
|
||||||
|
console.log('[People] - Collection save', collection)
|
||||||
|
if (collectionEditorMode.value === 'create') {
|
||||||
|
await handleCollectionCreate(collection, service)
|
||||||
|
} else {
|
||||||
|
await handleCollectionModify(collection)
|
||||||
|
}
|
||||||
|
await CollectionListRef.value?.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionCreate = async (collection: CollectionObject, service: ServiceObject) => {
|
||||||
|
console.log('[People] - Collection created', collection)
|
||||||
|
const result = await collectionsStore.create(service, collection)
|
||||||
|
|
||||||
|
if (result instanceof CollectionObject) {
|
||||||
|
selectedCollection.value = result
|
||||||
|
} else {
|
||||||
|
selectedCollection.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedEntity.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionModify = async (collection: CollectionObject) => {
|
||||||
|
console.log('[People] - Collection modified', collection)
|
||||||
|
await collectionsStore.modify(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollectionDelete = async (collection: CollectionObject) => {
|
||||||
|
console.log('[People] - Collection deleted', collection)
|
||||||
|
await collectionsStore.destroy(collection)
|
||||||
|
selectedCollection.value = null
|
||||||
|
selectedEntity.value = null
|
||||||
|
await CollectionListRef.value?.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEntitySelect = (entity: EntityObject) => {
|
||||||
|
console.log('[People] - Entity selected', entity)
|
||||||
|
selectedEntity.value = entity
|
||||||
|
entityEditorMode.value = 'view'
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEntityFresh = (type: string) => {
|
||||||
|
console.log('[People] - Entity create', type)
|
||||||
|
selectedEntity.value = entitiesStore.fresh(type)
|
||||||
|
entityEditorMode.value = 'edit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEntitySave = async (entity: EntityObject, collection?: CollectionObject | null) => {
|
||||||
|
console.log('[People] - Entity save', entity)
|
||||||
|
if (!(collection instanceof CollectionObject)) {
|
||||||
|
collection = selectedCollection.value
|
||||||
|
}
|
||||||
|
if (!collection) {
|
||||||
|
console.warn('[People] - Invalid collection object cannot save entity')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (entity.id === null) {
|
||||||
|
console.log('[People] - Entity create', entity)
|
||||||
|
entity.data.created = new Date();
|
||||||
|
selectedEntity.value = await entitiesStore.create(collection, entity)
|
||||||
|
} else if (entity.id !== null) {
|
||||||
|
console.log('[People] - Entity modify', entity)
|
||||||
|
entity.data.modified = new Date();
|
||||||
|
selectedEntity.value = await entitiesStore.modify(collection, entity)
|
||||||
|
}
|
||||||
|
entityEditorMode.value = 'view'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEntityDelete = async (entity: EntityObject, collection?: CollectionObject | null) => {
|
||||||
|
console.log('[People] - Entity deleted', entity)
|
||||||
|
if (!(collection instanceof CollectionObject)) {
|
||||||
|
collection = selectedCollection.value
|
||||||
|
}
|
||||||
|
if (!collection) {
|
||||||
|
console.warn('[People] - Invalid collection object cannot delete entity')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await entitiesStore.destroy(collection, entity)
|
||||||
|
selectedEntity.value = null
|
||||||
|
entityEditorMode.value = 'view'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="people-container">
|
||||||
|
<!-- Top Navigation Bar -->
|
||||||
|
<v-app-bar elevation="0" class="people-toolbar border-b">
|
||||||
|
<template #prepend>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-menu"
|
||||||
|
variant="text"
|
||||||
|
@click="sidebarVisible = !sidebarVisible"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-app-bar-title class="d-flex align-center">
|
||||||
|
<v-icon size="28" color="primary" class="mr-2">mdi-account-multiple</v-icon>
|
||||||
|
<span class="text-h6 font-weight-bold">People Manager</span>
|
||||||
|
</v-app-bar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="people-content">
|
||||||
|
<!-- Sidebar Navigation Drawer -->
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="sidebarVisible"
|
||||||
|
:permanent="display.mdAndUp.value"
|
||||||
|
:temporary="display.smAndDown.value"
|
||||||
|
width="280"
|
||||||
|
class="people-sidebar"
|
||||||
|
>
|
||||||
|
<div class="pa-4 d-flex flex-column h-100">
|
||||||
|
<CollectionList
|
||||||
|
ref="CollectionListRef"
|
||||||
|
:selected-collection="selectedCollection"
|
||||||
|
@select="handleCollectionSelect"
|
||||||
|
@edit="handleCollectionEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<v-icon icon="mdi-plus-circle" size="small" class="mr-2" />
|
||||||
|
<span class="text-subtitle-2 font-weight-bold">Create</span>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-account-plus"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
class="justify-start"
|
||||||
|
:disabled="!selectedCollection"
|
||||||
|
@click="onEntityFresh('individual')"
|
||||||
|
>
|
||||||
|
Individual
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-domain-plus"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
class="justify-start"
|
||||||
|
:disabled="!selectedCollection"
|
||||||
|
@click="onEntityFresh('organization')"
|
||||||
|
>
|
||||||
|
Organization
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-account-group"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
class="justify-start"
|
||||||
|
:disabled="!selectedCollection"
|
||||||
|
@click="onEntityFresh('group')"
|
||||||
|
>
|
||||||
|
Group
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-book-plus"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
class="justify-start"
|
||||||
|
@click="onCollectionFresh"
|
||||||
|
>
|
||||||
|
Address Book
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<!-- Main View -->
|
||||||
|
<div class="people-main">
|
||||||
|
<v-alert
|
||||||
|
v-if="!isPeopleManagerAvailable"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
closable
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-alert-title class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-alert-circle" class="mr-2" />
|
||||||
|
People Manager Not Available
|
||||||
|
</v-alert-title>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p>
|
||||||
|
The People Manager module is not installed or enabled.
|
||||||
|
This module requires the <strong>people_manager</strong> module to function properly.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 mb-0">
|
||||||
|
Please contact your system administrator to install and enable the
|
||||||
|
<code>people_manager</code> module.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<div v-if="isPeopleManagerAvailable" class="people-wrapper">
|
||||||
|
<div class="people-list-panel">
|
||||||
|
<PersonList
|
||||||
|
:selected-collection="selectedCollection"
|
||||||
|
:selected-entity="selectedEntity"
|
||||||
|
@select="handleEntitySelect"
|
||||||
|
@fresh="onEntityFresh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="people-editor-panel">
|
||||||
|
<PersonEditor
|
||||||
|
:mode="entityEditorMode"
|
||||||
|
:selected-collection="selectedCollection"
|
||||||
|
:selected-entity="selectedEntity"
|
||||||
|
@save="handleEntitySave"
|
||||||
|
@delete="handleEntityDelete"
|
||||||
|
@edit="handleEditorEdit"
|
||||||
|
@cancel="handleEditorCancel"
|
||||||
|
@close="handleEditorClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collection Editor Dialog -->
|
||||||
|
<CollectionEditor
|
||||||
|
v-model="showCollectionEditor"
|
||||||
|
:collection="editingCollection"
|
||||||
|
:mode="collectionEditorMode"
|
||||||
|
@save="handleCollectionSave"
|
||||||
|
@delete="handleCollectionDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.people-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
isolation: isolate; /* Create stacking context to prevent style leakage */
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-toolbar {
|
||||||
|
border-bottom: 1px solid rgb(var(--v-border-color)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-sidebar {
|
||||||
|
border-right: 1px solid rgb(var(--v-border-color)) !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-list-panel {
|
||||||
|
width: 300px;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 400px;
|
||||||
|
border-right: 1px solid rgb(var(--v-border-color));
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-editor-panel {
|
||||||
|
flex: 1;
|
||||||
|
border-left: 1px solid rgb(var(--v-border-color));
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.people-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-list-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
height: 40%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.people-editor-panel {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid rgb(var(--v-border-color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/routes.ts
Normal file
9
src/routes.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
name: 'people',
|
||||||
|
path: '/people',
|
||||||
|
component: () => import('@/pages/PeoplePage.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default routes
|
||||||
1
src/style.css
Normal file
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* people module styles */
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
20
tsconfig.app.json
Normal file
20
tsconfig.app.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@KTXC/*": ["../../core/src/*"],
|
||||||
|
"@PeopleManager/*": ["../people_manager/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
58
vite.config.ts
Normal file
58
vite.config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
{
|
||||||
|
name: 'inject-css-filename',
|
||||||
|
enforce: 'post',
|
||||||
|
generateBundle(_options, bundle) {
|
||||||
|
const cssFile = Object.keys(bundle).find(name => name.endsWith('.css'))
|
||||||
|
if (!cssFile) return
|
||||||
|
|
||||||
|
for (const fileName of Object.keys(bundle)) {
|
||||||
|
const chunk = bundle[fileName]
|
||||||
|
if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) {
|
||||||
|
chunk.code = chunk.code.replace(/__CSS_FILENAME_PLACEHOLDER__/g, `static/${cssFile}`)
|
||||||
|
console.log(`Injected CSS filename "static/${cssFile}" into ${fileName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'@KTXC': path.resolve(__dirname, '../../core/src'),
|
||||||
|
'@PeopleManager': path.resolve(__dirname, '../people_manager/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'static',
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/main.ts'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => 'module.mjs',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
'pinia',
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
if (assetInfo.name?.endsWith('.css')) {
|
||||||
|
return 'people-[hash].css'
|
||||||
|
}
|
||||||
|
return '[name]-[hash][extname]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user