The table view in our counselor dashboard works in a similar way to the Editor.
It is made to be dynamic and powered by a JSON schema.

The component is located at:
components/common/Table.vue

However, it’s wrappers are fractured within: components/counselor


@nuxt/ui

The table is powered by nuxt ui table. This gives us the UI and some functionality out of the box.
We still need to configure it to properly show our collections.

Schema

The schema for the different tables is in:
utils/tables.ts

This contains both the data map for which fields to show as well as computed formatting functions for more complicated rows we need to run processing on to view properly.

like in the editor, the table uses the url path to determine which schema to use. Since the table is the main page of the counselor app, the path is: https://tansy.app/{type}

Lets look at a specific example for the tasks table:
https://tansy.app/tasks

The schema:

Results in the table UI:


Switch Component

in components/counselor/dashboard.vue, this path is read and set:

// Determine which section to show based on route
const currentSection = computed(() => {
  const path = route.path;
  if (path.includes('/appointments')) return 'appointments';
  if (path.includes('/tasks')) return 'tasks';
  if (path.includes('/reports')) return 'reports';
  if (path.includes('/messages') || path.includes('/chat')) return 'messages';
  if (path.includes('/files')) return 'files';
  return 'members'; // default view
});

based on this value, we select the correct wrapper component:

<!-- Show different components based on current route -->
<CounselorMembers v-if="currentSection === 'members'" />
<CounselorAppointments v-if="currentSection === 'appointments'" />
<CounselorTasks v-if="currentSection === 'tasks'" />
<CounselorReports v-if="currentSection === 'reports'" />
<CounselorMessages v-if="currentSection === 'messages'" />
<CounselorFiles v-if="currentSection === 'files'" />

Wrapper Component

Lets look at the tasks component, located at components/counselor/tasks.vue

This component handles fetching the data and sending it to the table component it also handles the tabs for changing the query.

<template>
  <div class="k-tabs mb-4">
    <button
      v-for="tab in tabs"
      :key="tab.value"
      :class="['k-tab', activeTab === tab.value ? 'active' : '']"
      @click="activeTab = tab.value"
    >
      {{ tab.label }}
    </button>
  </div>

  <CommonTable
    :rows="tasks"
    type="tasks"
    :loading="loading"
    :clickable="true"
    :edit="true"
    @refresh="refresh"
  />
</template>

<script setup>
import { pb } from '#imports';

const tasks = ref([]);
const loading = ref(true);
const activeTab = ref(false);

const tabs = [
  { label: 'Open', value: false },
  { label: 'Done', value: true }
];

// Fetch tasks for the counselor

const fetchTasks = async () => {
  try {
    const records = await pb.collection('tasks').getFullList({
      filter: `completed = ${activeTab.value}`,
      sort: '-created',
      expand: 'member'
    });

    tasks.value = records;
  } catch (error) {
    console.error('Error fetching tasks:', error);
  } finally {
    loading.value = false;
  }
};

const refresh = () => {
  loading.value = true;
  fetchTasks();
}

watch(activeTab, () => {
  loading.value = true;
  fetchTasks();
});

onMounted(() => {
  fetchTasks();
});

</script>

The watch automatically refetches the data when the tab is changed.

Table Component

Looking inside the Table.vue component, it is type agnostic thanks to the data being managed in the individual wrapper components.

it handles things like mapping the data to the display schema in utils/tables.ts:

// This handles computed properties for the table
const computedRows = computed(() => {
  return props.rows.map((row: Record<string, any>) => ({
  ...row,
  ...(tableComputed[props.type]?.(row) || {})
}))

the filter search bar:

// This handles search
const searchQuery = ref('')
const filteredRows = computed(() => {
  if (!searchQuery.value) {
    return computedRows.value
  }

  return computedRows.value.filter((row: any) => {
    return Object.values(row).some((value: any) => {
      return String(value).toLowerCase().includes(searchQuery.value.toLowerCase())
    })
  })
})

and more like row clicks, and multi selecting items.


Adding a new table

In order to add a new table view to the app, you must:

  1. Create the schema in utils/tables.ts
  2. Add switch in components/counselors/dashboard.vue
  3. Create a wrapper at components/counselors/{type}.vue
  4. Create a new page for routing in pages/{type}/index.vue

This process may be simplified in the future with a more capable table component that can handle dynamic fetching for the different types, but as of now it needs the wrapper component.