chore: add missing multi-level editor for approval workflow, rename projects to campaings.

This commit is contained in:
2026-05-01 14:23:37 -04:00
parent 5077f557f4
commit 884ca4b96d
148 changed files with 11567 additions and 1383 deletions

View File

@@ -3,12 +3,12 @@
import { useI18n } from 'vue-i18n';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js';
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
const { t, locale } = useI18n();
const workspaceStore = useWorkspaceStore();
const projectsStore = useProjectsStore();
const campaignsStore = useCampaignsStore();
const contentItemsStore = useContentItemsStore();
const today = startOfDay(new Date());
@@ -17,42 +17,35 @@
const contentStatusMeta = {
Draft: { tone: 'production', readiness: 'building' },
'In internal review': { tone: 'approval', readiness: 'approval' },
'Changes requested internally': { tone: 'risk', readiness: 'rework' },
'Internal changes in progress': { tone: 'production', readiness: 'building' },
'Ready for client review': { tone: 'approval', readiness: 'approval' },
'In client review': { tone: 'approval', readiness: 'approval' },
'Changes requested by client': { tone: 'risk', readiness: 'rework' },
'Client changes in progress': { tone: 'production', readiness: 'building' },
'In production': { tone: 'production', readiness: 'building' },
'In approval': { tone: 'approval', readiness: 'approval' },
Approved: { tone: 'ready', readiness: 'ready' },
'Ready to publish': { tone: 'ready', readiness: 'ready' },
Scheduled: { tone: 'ready', readiness: 'scheduled' },
Published: { tone: 'published', readiness: 'published' },
Rejected: { tone: 'risk', readiness: 'blocked' },
Archived: { tone: 'muted', readiness: 'archived' },
};
const contentItemsByProjectId = computed(() => {
const contentItemsByCampaignId = computed(() => {
const grouped = new Map();
for (const item of contentItemsStore.items) {
const existing = grouped.get(item.projectId) ?? [];
const existing = grouped.get(item.campaignId) ?? [];
existing.push(item);
grouped.set(item.projectId, existing);
grouped.set(item.campaignId, existing);
}
return grouped;
});
const calendarEntries = computed(() => {
const projectEntries = projectsStore.projects
.filter(project => project.endDate || project.startDate)
.map(project => buildProjectEntry(project));
const campaignEntries = campaignsStore.campaigns
.filter(campaign => campaign.endDate || campaign.startDate)
.map(campaign => buildCampaignEntry(campaign));
const contentEntries = contentItemsStore.items
.filter(item => item.dueDate && item.status !== 'Archived')
.filter(item => item.dueDate)
.map(item => buildContentEntry(item));
return [...projectEntries, ...contentEntries].sort(sortByDate);
return [...campaignEntries, ...contentEntries].sort(sortByDate);
});
const entriesByDay = computed(() => {
@@ -126,11 +119,11 @@
});
const isLoading = computed(() =>
workspaceStore.isLoading || projectsStore.isLoading || contentItemsStore.isLoading
workspaceStore.isLoading || campaignsStore.isLoading || contentItemsStore.isLoading
);
const pageError = computed(() =>
workspaceStore.error || projectsStore.error || contentItemsStore.error
workspaceStore.error || campaignsStore.error || contentItemsStore.error
);
function buildDay(date, isOutsideMonth) {
@@ -147,13 +140,13 @@
function buildContentEntry(item) {
const statusMeta = contentStatusMeta[item.status] ?? { tone: 'production', readiness: 'building' };
const project = projectsStore.projects.find(candidate => candidate.id === item.projectId);
const campaign = campaignsStore.campaigns.find(candidate => candidate.id === item.campaignId);
return {
id: item.id,
type: 'content',
title: item.title,
subtitle: project?.name ?? t('dashboard.labels.unassignedProject'),
subtitle: campaign?.name ?? t('dashboard.labels.unassignedCampaign'),
scheduledAt: new Date(item.dueDate),
dayKey: dateKey(item.dueDate),
timeLabel: formatHour(item.dueDate),
@@ -162,22 +155,22 @@
};
}
function buildProjectEntry(project) {
const projectItems = contentItemsByProjectId.value.get(project.id) ?? [];
const approvedCount = projectItems.filter(item => ['Approved', 'Ready to publish', 'Published'].includes(item.status)).length;
function buildCampaignEntry(campaign) {
const campaignItems = contentItemsByCampaignId.value.get(campaign.id) ?? [];
const approvedCount = campaignItems.filter(item => ['Approved', 'Scheduled', 'Published'].includes(item.status)).length;
return {
id: project.id,
type: 'project',
title: project.name,
subtitle: projectItems.length
? t('dashboard.projectProgress', { scheduled: projectItems.length, approved: approvedCount })
id: campaign.id,
type: 'campaign',
title: campaign.name,
subtitle: campaignItems.length
? t('dashboard.campaignProgress', { scheduled: campaignItems.length, approved: approvedCount })
: t('dashboard.readiness.missing'),
scheduledAt: new Date(project.endDate ?? project.startDate),
dayKey: dateKey(project.endDate ?? project.startDate),
scheduledAt: new Date(campaign.endDate ?? campaign.startDate),
dayKey: dateKey(campaign.endDate ?? campaign.startDate),
timeLabel: t('dashboard.campaignDeadline'),
tone: projectItems.length ? 'project' : 'risk',
route: { name: 'campaign-detail', params: { projectId: project.id } },
tone: campaignItems.length ? 'campaign' : 'risk',
route: { name: 'campaign-detail', params: { campaignId: campaign.id } },
};
}
@@ -560,7 +553,7 @@
border-color: rgba(220, 38, 38, 0.16);
}
.calendar-entry.project {
.calendar-entry.campaign {
background: #f8fafc;
border-color: rgba(71, 85, 105, 0.18);
border-style: dashed;