Newer
Older
navi-1 / webclient / src / components / settings / skills / composables / useCatalogEntry.js
import { computed, ref } from 'vue'
import { useToast } from 'gnexus-ui-kit/vue'

import * as api from '@/api/index.js'
import { useConfirm } from '@/composables/useConfirm.js'
import { locale, t } from '@/i18n/index.js'
import { useAuthStore } from '@/stores/auth.js'

import { TEMPLATES } from '../constants.js'
import { errorText } from '../errors.js'

/**
 * The detail column: the selected entry, its reading view with history, and
 * the source editor that creates, saves and deletes documents.
 *
 * `loadError` is the list's banner ref — a failed detail fetch shows there,
 * above both panes, exactly as it did when this state lived in one setup.
 */
export function useCatalogEntry({ getKind, reload, loadError }) {
  const authStore = useAuthStore()
  const confirm = useConfirm()
  const toast = useToast()

  const entry = ref(null)
  const selectedId = ref(null)
  const detailLoading = ref(false)
  const editing = ref(false)
  const draft = ref('')
  const visibility = ref('personal')
  const saving = ref(false)
  const saveError = ref('')

  const versions = ref([])
  const showHistory = ref(false)

  // A selected entry or a new one takes the detail column; on a phone that also
  // replaces the list, so the reader gets the whole width.
  const detailOpen = computed(
    () => editing.value || entry.value !== null || detailLoading.value
  )
  const canEdit = computed(() => (editing.value && !entry.value ? true : Boolean(entry.value?.can_edit)))

  const visibilityOptions = computed(() => [
    { value: 'personal', label: t('settings.skillsPersonal') },
    { value: 'shared', label: t('settings.skillsShared') },
  ])

  async function select(id) {
    if (saving.value) return
    selectedId.value = id
    editing.value = false
    showHistory.value = false
    saveError.value = ''
    entry.value = null
    versions.value = []
    // `detailLoading` keeps the pane open on a phone while the entry travels.
    detailLoading.value = true
    try {
      const data = await api.getSkill(id)
      // The reader may have gone back or picked another row meanwhile.
      if (selectedId.value !== id) return
      entry.value = data
      draft.value = data.content
      visibility.value = data.visibility
    } catch (err) {
      if (selectedId.value === id) loadError.value = errorText(err)
    } finally {
      if (selectedId.value === id) detailLoading.value = false
    }
  }

  function startCreate() {
    entry.value = null
    selectedId.value = null
    detailLoading.value = false
    editing.value = true
    draft.value = TEMPLATES[getKind()]
    visibility.value = 'personal'
    versions.value = []
    showHistory.value = false
    saveError.value = ''
  }

  function beginEdit() {
    if (!entry.value) return
    draft.value = entry.value.content
    visibility.value = entry.value.visibility
    saveError.value = ''
    editing.value = true
  }

  function cancel() {
    if (entry.value) {
      editorBackToRead()
    } else {
      reset()
    }
  }

  function editorBackToRead() {
    draft.value = entry.value?.content || ''
    visibility.value = entry.value?.visibility || 'personal'
    saveError.value = ''
    editing.value = false
  }

  function reset() {
    entry.value = null
    selectedId.value = null
    detailLoading.value = false
    editing.value = false
    draft.value = ''
    saveError.value = ''
    versions.value = []
    showHistory.value = false
  }

  async function save() {
    saving.value = true
    saveError.value = ''
    try {
      if (entry.value) {
        const payload = { content: draft.value }
        if (authStore.isAdmin && visibility.value !== entry.value.visibility) {
          payload.visibility = visibility.value
        }
        const updated = await api.updateSkill(entry.value.id, payload)
        entry.value = updated
        draft.value = updated.content
        visibility.value = updated.visibility
        toast.success({ title: t('settings.skillsSaved') })
      } else {
        const created = await api.createSkill({
          content: draft.value,
          visibility: authStore.isAdmin ? visibility.value : 'personal',
        })
        entry.value = created
        selectedId.value = created.id
        draft.value = created.content
        visibility.value = created.visibility
        toast.success({ title: t('settings.skillsCreated') })
      }
      editing.value = false
      await reload()
    } catch (err) {
      // The server names what is missing in a protocol; that message is the
      // whole point of validating on save, so show it verbatim.
      saveError.value = errorText(err)
    } finally {
      saving.value = false
    }
  }

  async function remove() {
    if (!entry.value) return
    const ok = await confirm(t('settings.skillsDeleteConfirm', { name: entry.value.name }))
    if (!ok) return
    try {
      await api.deleteSkill(entry.value.id)
      toast.success({ title: t('settings.skillsDeleted') })
      reset()
      await reload()
    } catch (err) {
      toast.error({ title: t('common.error'), text: errorText(err) })
    }
  }

  async function toggleHistory() {
    showHistory.value = !showHistory.value
    if (!showHistory.value || versions.value.length || !entry.value) return
    try {
      const data = await api.getSkillVersions(entry.value.id)
      versions.value = data.items || []
    } catch (err) {
      saveError.value = errorText(err)
    }
  }

  function formatWhen(iso) {
    if (!iso) return ''
    const d = new Date(iso)
    return d.toLocaleString(locale.value, {
      year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
    })
  }

  return {
    authStore,
    entry,
    selectedId,
    detailLoading,
    editing,
    draft,
    visibility,
    saving,
    saveError,
    versions,
    showHistory,
    detailOpen,
    canEdit,
    visibilityOptions,
    select,
    startCreate,
    beginEdit,
    cancel,
    reset,
    save,
    remove,
    toggleHistory,
    formatWhen,
  }
}