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
scheduledfrom the localPostinterface andPOSTS_SELECT - Removes the
scheduledcolumn from thecolumnscomputed (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_postchips per day, coloured by status - Clicking a chip opens
PostDetailModalfor 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):
- Navigate to
/app/client/{id}→ Scheduled tab - Confirm week calendar appears above the post list
- If any platform posts have
scheduled_atset, their chips should appear on the correct day - Confirm no
scheduledcolumn in the post table - Navigate to Pending tab → also no
scheduledcolumn
✅ Done
All steps complete. Run the full verification checklist in the README.