Ai-Mee Help Centre
Home
Features
How-To Guides
FAQ
Need Help?
Home
Features
How-To Guides
FAQ
Need Help?

Step 7 — ClientOverview: Week Calendar on Scheduled Tab

File: front-end/src/components/pages/ClientOverview.vue
Prerequisites: Step 6 — Frontend Types & Store


What This Does

  • Removes scheduled from the local Post interface and POSTS_SELECT
  • Removes the scheduled column from the columns computed (both Pending and Scheduled tabs)
  • Adds a week-calendar view above the post table when the Scheduled tab is active
  • The calendar shows individual customer_platform_post chips per day, coloured by status
  • Clicking a chip opens PostDetailModal for that parent post

7.1 — Script changes

a) Add missing imports at the top of <script setup>

Add isoWeek plugin and PLATFORM_META (already have dayjs and relativeTime):

import isoWeek from 'dayjs/plugin/isoWeek'
import { PLATFORM_META } from '/@src/constants/platforms'
dayjs.extend(isoWeek)

b) Remove scheduled from POSTS_SELECT

Find:

const POSTS_SELECT = `
 id, title, html, platforms, scheduled, sent_at, status, created_at, updated_at,
 prompt, active, customer_id, email_list_id, generation_reason, context_sources,
 campaign:customer_campaign(id, name, customer_id)
 `

Replace with:

const POSTS_SELECT = `
 id, title, html, platforms, sent_at, status, created_at, updated_at,
 prompt, active, customer_id, email_list_id, generation_reason, context_sources,
 campaign:customer_campaign(id, name, customer_id)
 `

c) Remove scheduled?: string from the local Post interface

Find:

  interface Post {
    id: number
    title: string
    html?: string
    platforms: string[]
    scheduled?: string
    sent_at?: string

Replace with:

  interface Post {
    id: number
    title: string
    html?: string
    platforms: string[]
    sent_at?: string

d) Remove scheduled column from the columns computed

Find the if (activeTab.value === 'pending' || activeTab.value === 'scheduled') block:

if (activeTab.value === 'pending' || activeTab.value === 'scheduled') {
  base.push({
    key: 'scheduled',
    label: 'Scheduled',
    sortable: true,
    colClass: 'hidden md:table-cell',
  })
}

Delete this entire block.

e) Add platform posts state and calendar logic

Add the following new refs, interface, and functions before the fetchData function:

// ── Scheduled tab: per-platform calendar ─────────────────────────────────

interface PlatformPostItem {
  id: number
  platform: string
  scheduled_at: string
  sent_at: string | null
  customer_post_id: number
  post_title: string | null
}

const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const weekOffset = ref(0)
const platformPostItems = ref<PlatformPostItem[]>([])
const isPlatformPostsLoading = ref(false)
const platformPostsLoaded = ref(false)

const weekDates = computed(() => {
  const start = dayjs().add(weekOffset.value, 'week').startOf('isoWeek')
  return Array.from({ length: 7 }, (_, i) => start.add(i, 'day'))
})

const weekLabel = computed(() => {
  const s = weekDates.value[0]
  const e = weekDates.value[6]
  return s.month() === e.month()
    ? `${s.format('MMM D')} – ${e.format('D, YYYY')}`
    : `${s.format('MMM D')} – ${e.format('MMM D, YYYY')}`
})

const todayStr = dayjs().format('YYYY-MM-DD')

const platformPostsByDate = computed<Record<string, PlatformPostItem[]>>(() =>
  platformPostItems.value.reduce<Record<string, PlatformPostItem[]>>((acc, item) => {
    const key = dayjs(item.scheduled_at).format('YYYY-MM-DD')
    ;(acc[key] ||= []).push(item)
    return acc
  }, {})
)

const fetchPlatformPosts = async () => {
  if (isPlatformPostsLoading.value) return
  isPlatformPostsLoading.value = true
  try {
    const { data, error } = await supabase
      .from('customer_platform_post')
      .select(
        'id, platform, scheduled_at, sent_at, customer_post_id, customer_posts!inner(id, title, customer_id)'
      )
      .eq('customer_posts.customer_id', clientId.value)
      .not('scheduled_at', 'is', null)
      .order('scheduled_at', { ascending: true })

    if (error) throw error

    platformPostItems.value = (data ?? []).map((row: any) => ({
      id: row.id as number,
      platform: row.platform as string,
      scheduled_at: row.scheduled_at as string,
      sent_at: (row.sent_at as string | null) ?? null,
      customer_post_id: row.customer_post_id as number,
      post_title: (row.customer_posts as any)?.title ?? null,
    }))
    platformPostsLoaded.value = true
  } catch (e) {
    console.error('Error fetching platform posts:', e)
  } finally {
    isPlatformPostsLoading.value = false
  }
}

watch(activeTab, (tab) => {
  if (tab === 'scheduled' && !platformPostsLoaded.value) {
    fetchPlatformPosts()
  }
})

Also update onPostUpdated to refresh platform posts when a post is updated:

const onPostUpdated = async () => {
  platformPostsLoaded.value = false
  if (activeTab.value === 'scheduled') {
    await fetchPlatformPosts()
  }
}

7.2 — Template changes

a) Remove #cell-scheduled template slot

Find and delete from the template:

<template #cell-scheduled="{ row }">
  <span v-if="row.scheduled" class="text-muted-foreground">
    {{ dayjs(row.scheduled).format('MMM D, YYYY h:mm A') }}
  </span>
  <span v-else class="text-muted-foreground">—</span>
</template>

b) Add the week calendar above the AppDataTable

Find the start of the Posts CardContent section. Inside <CardContent class="pt-4">, directly before the <fieldset v-if="activeTab === 'sent'"> block, add:

        <!-- ── Scheduled Tab: Per-Platform Week Calendar ──────────── -->
        <div v-if="activeTab === 'scheduled'" class="mb-4">
          <VLoader :active="isPlatformPostsLoading" size="small">
            <Card>
              <CardHeader class="pb-2">
                <div class="flex items-center justify-between flex-wrap gap-2">
                  <div>
                    <p class="font-semibold text-sm">Platform Post Schedule</p>
                    <p class="text-xs text-muted-foreground">{{ weekLabel }}</p>
                  </div>
                  <div class="flex items-center gap-1">
                    <Button variant="outline" size="sm" class="h-7 w-7 p-0" @click="weekOffset--">
                      <iconify-icon icon="lucide:chevron-left" class="text-sm" />
                    </Button>
                    <Button
                      variant="outline"
                      size="sm"
                      class="h-7 px-2 text-xs"
                      @click="weekOffset = 0"
                    >
                      Today
                    </Button>
                    <Button variant="outline" size="sm" class="h-7 w-7 p-0" @click="weekOffset++">
                      <iconify-icon icon="lucide:chevron-right" class="text-sm" />
                    </Button>
                  </div>
                </div>
              </CardHeader>
              <CardContent class="p-0">
                <div class="grid grid-cols-7 border-t">
                  <div
                    v-for="(date, i) in weekDates"
                    :key="i"
                    class="border-r last:border-r-0 min-w-0"
                  >
                    <!-- Day header -->
                    <div
                      class="px-1 py-2 text-center border-b"
                      :class="date.format('YYYY-MM-DD') === todayStr ? 'bg-primary/5' : ''"
                    >
                      <p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wide">
                        {{ DAY_LABELS[i] }}
                      </p>
                      <div class="flex justify-center mt-0.5">
                        <span
                          class="text-sm font-bold size-6 flex items-center justify-center rounded-full"
                          :class="
                            date.format('YYYY-MM-DD') === todayStr
                              ? 'bg-primary text-primary-foreground'
                              : ''
                          "
                        >
                          {{ date.format('D') }}
                        </span>
                      </div>
                    </div>
                    <!-- Platform posts for this day -->
                    <div class="min-h-[88px] p-1 space-y-1">
                      <Tippy
                        v-for="item in platformPostsByDate[date.format('YYYY-MM-DD')] ?? []"
                        :key="item.id"
                        :content="item.post_title || 'Untitled'"
                        placement="top"
                      >
                        <button
                          class="w-full text-left rounded px-1.5 py-1 text-[10px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 min-w-0 cursor-pointer"
                          :class="
                            item.sent_at
                              ? 'bg-success/15 text-success'
                              : 'bg-primary/10 text-primary'
                          "
                          @click="openPostModal(item.customer_post_id)"
                        >
                          <iconify-icon
                            :icon="PLATFORM_META[item.platform]?.icon ?? 'lucide:share-2'"
                            class="shrink-0 text-sm"
                            :style="{ color: PLATFORM_META[item.platform]?.color }"
                          />
                          <span class="truncate">{{ item.post_title || '…' }}</span>
                        </button>
                      </Tippy>
                    </div>
                  </div>
                </div>
              </CardContent>
            </Card>
          </VLoader>
        </div>

7.3 — Verify

cd front-end
pnpm test:tsc
pnpm test:unit

Then do a visual check (see instructions header for the Vite dev server and Playwright screenshot steps):

  1. Navigate to /app/client/{id} → Scheduled tab
  2. Confirm week calendar appears above the post list
  3. If any platform posts have scheduled_at set, their chips should appear on the correct day
  4. Confirm no scheduled column in the post table
  5. Navigate to Pending tab → also no scheduled column

✅ Done

All steps complete. Run the full verification checklist in the README.