chore: add missing multi-level editor for approval workflow, rename projects to campaings.
This commit is contained in:
232
frontend/src/features/campaigns/views/CampaignDetailView.vue
Normal file
232
frontend/src/features/campaigns/views/CampaignDetailView.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const route = useRoute();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const campaignsStore = useCampaignsStore();
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
|
||||
const campaign = computed(() =>
|
||||
campaignsStore.campaigns.find(candidate => candidate.id === route.params.campaignId) ?? null
|
||||
);
|
||||
|
||||
const scopedItems = computed(() =>
|
||||
contentItemsStore.items
|
||||
.filter(item => item.campaignId === route.params.campaignId)
|
||||
.sort((left, right) => {
|
||||
const leftDue = left.dueDate ? new Date(left.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
|
||||
const rightDue = right.dueDate ? new Date(right.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
|
||||
return leftDue - rightDue;
|
||||
})
|
||||
);
|
||||
|
||||
function formatCampaignDateRange(campaignValue) {
|
||||
if (!campaignValue?.startDate || !campaignValue?.endDate) {
|
||||
return 'No date range';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).formatRange(new Date(campaignValue.startDate), new Date(campaignValue.endDate));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-shell">
|
||||
<div
|
||||
v-if="!campaign"
|
||||
class="page-message error"
|
||||
>
|
||||
The selected campaign could not be found in the active workspace.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="hero">
|
||||
<div>
|
||||
<div class="breadcrumb-row">
|
||||
<router-link
|
||||
class="breadcrumb"
|
||||
:to="{ name: 'workspace-dashboard' }"
|
||||
>
|
||||
Workspace
|
||||
</router-link>
|
||||
<span>/</span>
|
||||
<router-link
|
||||
class="breadcrumb"
|
||||
:to="{ name: 'campaigns' }"
|
||||
>
|
||||
Campaigns
|
||||
</router-link>
|
||||
</div>
|
||||
<h1>{{ campaign.name }}</h1>
|
||||
<p>{{ campaign.description || `${workspaceStore.activeWorkspace?.name} delivery stream with only the content scheduled in this campaign.` }}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-meta">
|
||||
<div class="meta-chip">{{ campaign.status }}</div>
|
||||
<div class="meta-copy">{{ formatCampaignDateRange(campaign) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="campaign.notes"
|
||||
class="page-message"
|
||||
>
|
||||
{{ campaign.notes }}
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<strong>Content items</strong>
|
||||
<span>{{ scopedItems.length }} scheduled in this campaign</span>
|
||||
</div>
|
||||
|
||||
<div class="scope-actions">
|
||||
<router-link
|
||||
v-if="authStore.isManager || authStore.isProvider"
|
||||
:to="{ name: 'content-item-create', query: { campaignId: campaign.id } }"
|
||||
class="scope-button"
|
||||
>
|
||||
New content in {{ campaign.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="scopedItems.length"
|
||||
class="content-grid"
|
||||
>
|
||||
<router-link
|
||||
v-for="item in scopedItems"
|
||||
:key="item.id"
|
||||
:to="{ name: 'content-item-detail', params: { id: item.id } }"
|
||||
class="content-card"
|
||||
>
|
||||
<div class="version-chip">{{ item.currentRevisionLabel }}</div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.publicationTargets }}</span>
|
||||
<div class="status-row">
|
||||
<em>{{ item.status }}</em>
|
||||
<small>{{ item.dueDate ? new Date(item.dueDate).toLocaleDateString() : 'No due date' }}</small>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="page-message"
|
||||
>
|
||||
No content items are attached to this campaign yet.
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-shell {
|
||||
@apply mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.content-card {
|
||||
@apply rounded-[1.5rem] border;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.hero {
|
||||
@apply flex flex-col gap-5 p-6 lg:flex-row lg:items-start lg:justify-between;
|
||||
}
|
||||
|
||||
.breadcrumb-row {
|
||||
@apply flex items-center gap-2 text-sm;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.breadcrumb,
|
||||
.hero p,
|
||||
.meta-copy,
|
||||
.section-header span,
|
||||
.content-card span,
|
||||
.status-row small,
|
||||
.status-row em {
|
||||
@apply text-sm leading-6 not-italic;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
@apply font-bold uppercase tracking-[0.16em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header strong,
|
||||
.content-card strong {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
@apply mt-2 text-4xl font-black;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
@apply flex flex-wrap items-start gap-3;
|
||||
}
|
||||
|
||||
.meta-chip,
|
||||
.version-chip {
|
||||
@apply rounded-full px-4 py-2 text-xs font-bold uppercase tracking-[0.16em];
|
||||
background: rgba(23, 32, 51, 0.08);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@apply flex items-center justify-between gap-4;
|
||||
}
|
||||
|
||||
.scope-actions {
|
||||
@apply flex justify-start;
|
||||
}
|
||||
|
||||
.scope-button {
|
||||
@apply inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-bold no-underline transition;
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.scope-button:hover {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.section-header strong {
|
||||
@apply text-lg font-black;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
@apply flex flex-col gap-4 p-5 no-underline;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
@apply flex items-center justify-between gap-3;
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply rounded-[1.25rem] border p-4 text-sm font-medium;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.page-message.error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user