213 lines
7.3 KiB
Vue
213 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref } from "vue";
|
|
import ContactCollaborativeEditor from "~~/app/components/ContactCollaborativeEditor.client.vue";
|
|
|
|
type DocumentSortOption = {
|
|
value: string;
|
|
label: string;
|
|
};
|
|
|
|
type DocumentListItem = {
|
|
id: string;
|
|
title: string;
|
|
scope: string;
|
|
summary: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
type SelectedDocument = {
|
|
id: string;
|
|
title: string;
|
|
scope: string;
|
|
owner: string;
|
|
summary: string;
|
|
body: string;
|
|
};
|
|
|
|
const props = defineProps<{
|
|
documentSearch: string;
|
|
documentSortMode: string;
|
|
documentSortOptions: DocumentSortOption[];
|
|
filteredDocuments: DocumentListItem[];
|
|
selectedDocumentId: string;
|
|
selectedDocument: SelectedDocument | null;
|
|
formatDocumentScope: (scope: string) => string;
|
|
formatStamp: (iso: string) => string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: "update:documentSearch", value: string): void;
|
|
(e: "update:documentSortMode", value: string): void;
|
|
(e: "select-document", documentId: string): void;
|
|
(e: "update-selected-document-body", value: string): void;
|
|
(e: "delete-document", documentId: string): void;
|
|
}>();
|
|
|
|
const documentContextMenu = ref<{
|
|
open: boolean;
|
|
x: number;
|
|
y: number;
|
|
documentId: string;
|
|
}>({
|
|
open: false,
|
|
x: 0,
|
|
y: 0,
|
|
documentId: "",
|
|
});
|
|
|
|
function closeDocumentContextMenu() {
|
|
if (!documentContextMenu.value.open) return;
|
|
documentContextMenu.value = {
|
|
open: false,
|
|
x: 0,
|
|
y: 0,
|
|
documentId: "",
|
|
};
|
|
}
|
|
|
|
function openDocumentContextMenu(event: MouseEvent, doc: DocumentListItem) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
emit("select-document", doc.id);
|
|
const padding = 8;
|
|
const menuWidth = 176;
|
|
const menuHeight = 44;
|
|
const maxX = Math.max(padding, window.innerWidth - menuWidth - padding);
|
|
const maxY = Math.max(padding, window.innerHeight - menuHeight - padding);
|
|
documentContextMenu.value = {
|
|
open: true,
|
|
x: Math.min(maxX, Math.max(padding, event.clientX)),
|
|
y: Math.min(maxY, Math.max(padding, event.clientY)),
|
|
documentId: doc.id,
|
|
};
|
|
}
|
|
|
|
function deleteDocumentFromContextMenu() {
|
|
const documentId = documentContextMenu.value.documentId;
|
|
if (!documentId) return;
|
|
emit("delete-document", documentId);
|
|
closeDocumentContextMenu();
|
|
}
|
|
|
|
function onWindowKeydown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") {
|
|
closeDocumentContextMenu();
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("keydown", onWindowKeydown);
|
|
window.addEventListener("scroll", closeDocumentContextMenu, true);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener("keydown", onWindowKeydown);
|
|
window.removeEventListener("scroll", closeDocumentContextMenu, true);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="flex h-full min-h-0 flex-col gap-0" @click="closeDocumentContextMenu">
|
|
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)]">
|
|
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col">
|
|
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
|
<div class="flex items-center gap-1">
|
|
<input
|
|
:value="props.documentSearch"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full"
|
|
placeholder="Search documents"
|
|
@input="emit('update:documentSearch', ($event.target as HTMLInputElement).value)"
|
|
>
|
|
|
|
<div class="dropdown dropdown-end">
|
|
<button
|
|
tabindex="0"
|
|
class="btn btn-ghost btn-sm btn-square"
|
|
title="Sort documents"
|
|
>
|
|
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
|
<path d="M3 5h18v2H3zm3 6h12v2H6zm4 6h4v2h-4z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div tabindex="0" class="dropdown-content z-20 mt-2 w-44 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
|
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Sort docs</p>
|
|
<button
|
|
v-for="option in props.documentSortOptions"
|
|
:key="`document-sort-${option.value}`"
|
|
class="btn btn-ghost btn-sm w-full justify-between"
|
|
@click="emit('update:documentSortMode', option.value)"
|
|
>
|
|
<span>{{ option.label }}</span>
|
|
<span v-if="props.documentSortMode === option.value">✓</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="min-h-0 flex-1 overflow-y-auto p-0">
|
|
<button
|
|
v-for="doc in props.filteredDocuments"
|
|
:key="doc.id"
|
|
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
|
:class="props.selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
|
@click="emit('select-document', doc.id)"
|
|
@contextmenu="openDocumentContextMenu($event, doc)"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
|
</div>
|
|
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ props.formatDocumentScope(doc.scope) }}</p>
|
|
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
|
<p class="mt-1 text-[10px] text-base-content/55">Updated {{ props.formatStamp(doc.updatedAt) }}</p>
|
|
</button>
|
|
<p v-if="props.filteredDocuments.length === 0" class="px-2 py-2 text-xs text-base-content/55">
|
|
No documents found.
|
|
</p>
|
|
</div>
|
|
</aside>
|
|
|
|
<article class="h-full min-h-0 flex flex-col">
|
|
<div v-if="props.selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
|
|
<div class="border-b border-base-300 pb-2">
|
|
<p class="font-medium">{{ props.selectedDocument.title }}</p>
|
|
<p class="text-xs text-base-content/60">
|
|
{{ props.formatDocumentScope(props.selectedDocument.scope) }} · {{ props.selectedDocument.owner }}
|
|
</p>
|
|
<p class="mt-1 text-sm text-base-content/80">{{ props.selectedDocument.summary }}</p>
|
|
</div>
|
|
|
|
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
|
<ContactCollaborativeEditor
|
|
:key="`doc-editor-${props.selectedDocument.id}`"
|
|
:model-value="props.selectedDocument.body"
|
|
:room="`crm-doc-${props.selectedDocument.id}`"
|
|
placeholder="Describe policy, steps, rules, and exceptions..."
|
|
@update:model-value="emit('update-selected-document-body', $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
|
|
No document selected.
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div
|
|
v-if="documentContextMenu.open"
|
|
class="fixed z-50 w-44 rounded-lg border border-base-300 bg-base-100 p-1 shadow-xl"
|
|
:style="{ left: `${documentContextMenu.x}px`, top: `${documentContextMenu.y}px` }"
|
|
@click.stop
|
|
>
|
|
<button
|
|
class="btn btn-ghost btn-sm w-full justify-start text-error"
|
|
@click="deleteDocumentFromContextMenu"
|
|
>
|
|
Delete document
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|