feat: pivot to social media workflow app
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 12:58:35 -04:00
parent 0f4acc1b6d
commit df3e602015
349 changed files with 18685 additions and 16010 deletions

View File

@@ -24,6 +24,20 @@ export const useAuthStore = defineStore('auth', () => {
const isAuthenticated = computed(() => !!accessToken.value);
const userId = computed(() => tokenClaims.value?.sub);
const userRoles = computed(() => {
const claims = tokenClaims.value ?? {};
const candidates = [
claims.role,
claims.roles,
claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'],
].flatMap(value => Array.isArray(value) ? value : value ? [value] : []);
return [...new Set(candidates)];
});
const persona = computed(() => tokenClaims.value?.persona ?? null);
const isManager = computed(() => userRoles.value.includes('Administrator') || userRoles.value.includes('Manager'));
const isClient = computed(() => userRoles.value.includes('Client'));
const isProvider = computed(() => userRoles.value.includes('Provider'));
function updateTokens(data) {
if (!data?.accessToken || !data?.refreshToken) {
@@ -259,11 +273,21 @@ export const useAuthStore = defineStore('auth', () => {
}
}
function hasAnyRole(roles) {
return roles.some(role => userRoles.value.includes(role));
}
return {
accessToken,
refreshToken,
isAuthenticated,
userId,
userRoles,
persona,
hasAnyRole,
isManager,
isClient,
isProvider,
isRefreshing,
login,
loginWithGoogle,

View File

@@ -1,96 +0,0 @@
import {defineStore} from 'pinia'
import {useClient} from "@/plugins/api.js";
import {useSessionStorage} from "@vueuse/core";
import {ref, watch} from "vue";
import {useRoute, useRouter} from "vue-router";
export const useBrandingStore = defineStore(
'branding',
() => {
const currentBrand = ref(undefined)
const loading = ref(false)
const error = ref(null)
const notFound = ref(false)
const value = useSessionStorage(
'branding',
{},
{writeDefaults: false})
const presentationInfos = ref([])
const router = useRouter()
const route = useRoute()
watch(
() => route.params.creator,
async (creator) => {
// Extract just the creator name from the path (remove any additional segments)
const creatorName = creator ? creator.split('/')[0] : undefined;
await updateBrand(creatorName);
}
)
async function updateBrand(newBrand) {
loading.value = true
error.value = null
notFound.value = false
if (newBrand !== currentBrand.value) {
if (newBrand !== undefined) {
const result = await fetchCreatorData(newBrand)
if (result.success) {
value.value = result.data
currentBrand.value = newBrand
presentationInfos.value = result.data?.presentationInfos
} else {
// Handle different error types
if (result.status === 404) {
notFound.value = true
error.value = 'Creator not found'
} else {
error.value = result.error || 'Failed to load creator'
}
value.value = {}
currentBrand.value = undefined
presentationInfos.value = []
}
} else {
value.value = {}
currentBrand.value = undefined
presentationInfos.value = []
}
}
loading.value = false
}
const fetchCreatorData = async (creatorAlias) => {
try {
const client = useClient()
const response = await client.get(`/api/creators/@${creatorAlias}`)
return { success: true, data: response.data }
} catch (error) {
console.error('Error fetching creator data:', error)
if (error.response?.status === 404) {
return { success: false, status: 404, error: 'Creator not found' }
}
return {
success: false,
status: error.response?.status || 500,
error: error.message || 'Unknown error occurred'
}
}
}
return {
currentBrand,
value,
loading,
error,
notFound,
updateBrand,
presentationInfos
}
})

View File

@@ -0,0 +1,122 @@
import { computed } from 'vue';
import { defineStore } from 'pinia';
import { useSessionStorage } from '@vueuse/core';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useContentItemsStore } from '@/stores/contentItemsStore.js';
export const useChannelsStore = defineStore('channels', () => {
const workspaceStore = useWorkspaceStore();
const contentItemsStore = useContentItemsStore();
const customChannelsByWorkspace = useSessionStorage('workspace-custom-channels', {}, {
serializer: {
read: value => (value ? JSON.parse(value) : {}),
write: value => JSON.stringify(value ?? {}),
},
});
const channels = computed(() => {
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
if (!currentWorkspaceId) {
return [];
}
const derivedChannels = new Map();
const customChannels = customChannelsByWorkspace.value[currentWorkspaceId] ?? [];
for (const item of contentItemsStore.items) {
for (const name of parseTargets(item.publicationTargets)) {
const key = slugify(name);
const existing = derivedChannels.get(key) ?? {
id: key,
name,
network: null,
source: 'derived',
};
derivedChannels.set(key, existing);
}
}
for (const channel of customChannels) {
derivedChannels.set(channel.id, {
...channel,
source: 'custom',
});
}
return [...derivedChannels.values()].sort((left, right) => left.name.localeCompare(right.name));
});
const availableNetworks = [
'Instagram',
'TikTok',
'Facebook',
'LinkedIn',
'YouTube',
'X',
'Reddit',
'Website',
];
function createChannel(payload) {
const currentWorkspaceId = workspaceStore.activeWorkspaceId;
if (!currentWorkspaceId) {
throw new Error('An active workspace is required to create a channel.');
}
const normalizedName = payload.name.trim();
const normalizedNetwork = payload.network.trim();
if (!normalizedName) {
throw new Error('Channel name is required.');
}
if (!normalizedNetwork) {
throw new Error('Network is required.');
}
if (!availableNetworks.includes(normalizedNetwork)) {
throw new Error('Selected network is invalid.');
}
const existing = channels.value.some(channel =>
channel.name.toLowerCase() === normalizedName.toLowerCase()
&& (channel.network ?? '').toLowerCase() === normalizedNetwork.toLowerCase()
);
if (existing) {
throw new Error('A channel with this name already exists for the selected network.');
}
const next = customChannelsByWorkspace.value[currentWorkspaceId] ?? [];
customChannelsByWorkspace.value = {
...customChannelsByWorkspace.value,
[currentWorkspaceId]: [
...next,
{
id: slugify(`${normalizedNetwork}-${normalizedName}`),
name: normalizedName,
network: normalizedNetwork,
},
],
};
}
function parseTargets(value) {
return (value ?? '')
.split(/[,\n]+/)
.map(target => target.trim())
.filter(Boolean);
}
function slugify(value) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
return {
availableNetworks,
channels,
createChannel,
};
});

View File

@@ -0,0 +1,182 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from '@/stores/authStore.js';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js';
export const useClientsStore = defineStore('clients', () => {
const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore();
const client = useClient();
const clients = ref([]);
const isLoading = ref(false);
const isCreating = ref(false);
const isUpdating = ref(false);
const isUploadingPortrait = ref(false);
const error = ref(null);
const operationalClient = computed(() => {
if (!clients.value.length) {
return null;
}
return clients.value.find(candidate => candidate.name === workspaceStore.activeWorkspace?.name)
?? clients.value[0];
});
async function fetchClients() {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
clients.value = [];
error.value = null;
return;
}
isLoading.value = true;
error.value = null;
try {
const response = await client.get('/api/clients', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
},
});
clients.value = response.data ?? [];
} catch (fetchError) {
console.error('Failed to fetch clients:', fetchError);
clients.value = [];
error.value = 'Failed to load clients.';
} finally {
isLoading.value = false;
}
}
async function createClient(payload) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to create a client.');
}
if (isCreating.value) {
throw new Error('A client creation request is already in progress.');
}
isCreating.value = true;
error.value = null;
try {
const response = await client.post('/api/clients', {
...payload,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
clients.value = [...clients.value, response.data]
.sort((left, right) => left.name.localeCompare(right.name));
}
return response.data;
} catch (createError) {
console.error('Failed to create client:', createError);
error.value = 'Failed to create client.';
throw createError;
} finally {
isCreating.value = false;
}
}
async function updateClient(clientId, payload) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to update a client.');
}
if (isUpdating.value) {
throw new Error('A client update request is already in progress.');
}
isUpdating.value = true;
error.value = null;
try {
const response = await client.put(`/api/clients/${clientId}`, payload);
if (response.data) {
clients.value = clients.value
.map(candidate => candidate.id === clientId ? response.data : candidate)
.sort((left, right) => left.name.localeCompare(right.name));
}
return response.data;
} catch (updateError) {
console.error('Failed to update client:', updateError);
error.value = 'Failed to update client.';
throw updateError;
} finally {
isUpdating.value = false;
}
}
async function uploadClientPortrait(clientId, file) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to upload a client logo.');
}
if (isUploadingPortrait.value) {
throw new Error('A client logo upload is already in progress.');
}
isUploadingPortrait.value = true;
error.value = null;
try {
const formData = new FormData();
formData.append('file', file, file.name || 'client-logo.png');
const response = await client.post(`/api/clients/${clientId}/portrait`, formData);
const blobUrl = response.data?.blobUrl;
if (blobUrl) {
clients.value = clients.value.map(candidate =>
candidate.id === clientId
? { ...candidate, portraitUrl: `${blobUrl}?${Date.now()}` }
: candidate
);
}
return response.data;
} catch (uploadError) {
console.error('Failed to upload client logo:', uploadError);
error.value = 'Failed to upload client logo.';
throw uploadError;
} finally {
isUploadingPortrait.value = false;
}
}
watch(
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
async ([isAuthenticated, workspaceId]) => {
if (!isAuthenticated || !workspaceId) {
clients.value = [];
error.value = null;
return;
}
await fetchClients();
},
{ immediate: true }
);
return {
clients,
operationalClient,
isLoading,
isCreating,
isUpdating,
isUploadingPortrait,
error,
fetchClients,
createClient,
updateClient,
uploadClientPortrait,
};
});

View File

@@ -0,0 +1,255 @@
import { reactive, ref } from 'vue';
import { defineStore } from 'pinia';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js';
export const useContentItemDetailStore = defineStore('content-item-detail', () => {
const workspaceStore = useWorkspaceStore();
const client = useClient();
const item = ref(null);
const revisions = ref([]);
const assets = ref([]);
const comments = ref([]);
const approvals = ref([]);
const notifications = ref([]);
const isLoading = ref(false);
const error = ref(null);
const actions = reactive({
revision: false,
asset: false,
assetRevision: false,
comment: false,
approval: false,
decision: false,
status: false,
});
function reset() {
item.value = null;
revisions.value = [];
assets.value = [];
comments.value = [];
approvals.value = [];
notifications.value = [];
error.value = null;
}
async function fetchContentItemDetail(contentItemId) {
isLoading.value = true;
error.value = null;
try {
const [
itemResponse,
revisionsResponse,
assetsResponse,
commentsResponse,
approvalsResponse,
notificationsResponse,
] = await Promise.all([
client.get(`/api/content-items/${contentItemId}`),
client.get(`/api/content-items/${contentItemId}/revisions`),
client.get('/api/assets', { params: { contentItemId } }),
client.get('/api/comments', { params: { contentItemId } }),
client.get('/api/approvals', { params: { contentItemId } }),
client.get('/api/notifications', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
contentItemId,
},
}),
]);
item.value = itemResponse.data;
revisions.value = revisionsResponse.data ?? [];
assets.value = assetsResponse.data ?? [];
comments.value = commentsResponse.data ?? [];
approvals.value = approvalsResponse.data ?? [];
notifications.value = notificationsResponse.data ?? [];
} catch (fetchError) {
console.error('Failed to load content item detail:', fetchError);
reset();
error.value = 'Failed to load the content item detail.';
} finally {
isLoading.value = false;
}
}
async function createRevision(contentItemId, payload) {
actions.revision = true;
try {
const response = await client.post(`/api/content-items/${contentItemId}/revisions`, payload);
if (response.data) {
revisions.value = [response.data, ...revisions.value];
await fetchContentItemDetail(contentItemId);
}
return response.data;
} finally {
actions.revision = false;
}
}
async function addGoogleDriveAsset(contentItemId, payload) {
actions.asset = true;
try {
const response = await client.post('/api/assets/google-drive', {
...payload,
contentItemId,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
assets.value = [...assets.value, response.data];
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.asset = false;
}
}
async function addAssetRevision(contentItemId, assetId, payload) {
actions.assetRevision = true;
try {
const response = await client.post(`/api/assets/${assetId}/revisions`, payload);
if (response.data) {
await fetchAssets(contentItemId);
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.assetRevision = false;
}
}
async function addComment(contentItemId, payload) {
actions.comment = true;
try {
const response = await client.post('/api/comments', {
...payload,
contentItemId,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
comments.value = [...comments.value, response.data];
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.comment = false;
}
}
async function resolveComment(contentItemId, commentId) {
actions.comment = true;
try {
const response = await client.post(`/api/comments/${commentId}/resolve`);
if (response.data) {
comments.value = comments.value.map(comment => comment.id === commentId ? response.data : comment);
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.comment = false;
}
}
async function createApproval(contentItemId, payload) {
actions.approval = true;
try {
const response = await client.post('/api/approvals', {
...payload,
contentItemId,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
approvals.value = [response.data, ...approvals.value];
await fetchContentItem(contentItemId);
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.approval = false;
}
}
async function submitDecision(contentItemId, approvalId, payload) {
actions.decision = true;
try {
const response = await client.post(`/api/approvals/${approvalId}/decisions`, payload);
if (response.data) {
approvals.value = approvals.value.map(approval => approval.id === approvalId ? response.data : approval);
await fetchContentItem(contentItemId);
await fetchNotifications(contentItemId);
}
return response.data;
} finally {
actions.decision = false;
}
}
async function updateStatus(contentItemId, status) {
actions.status = true;
try {
const response = await client.post(`/api/content-items/${contentItemId}/status`, { status });
item.value = response.data;
await fetchNotifications(contentItemId);
return response.data;
} finally {
actions.status = false;
}
}
async function fetchContentItem(contentItemId) {
const response = await client.get(`/api/content-items/${contentItemId}`);
item.value = response.data;
return response.data;
}
async function fetchAssets(contentItemId) {
const response = await client.get('/api/assets', { params: { contentItemId } });
assets.value = response.data ?? [];
return assets.value;
}
async function fetchNotifications(contentItemId) {
const response = await client.get('/api/notifications', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
contentItemId,
},
});
notifications.value = response.data ?? [];
return notifications.value;
}
return {
item,
revisions,
assets,
comments,
approvals,
notifications,
isLoading,
error,
actions,
reset,
fetchContentItemDetail,
createRevision,
addGoogleDriveAsset,
addAssetRevision,
addComment,
resolveComment,
createApproval,
submitDecision,
updateStatus,
};
});

View File

@@ -0,0 +1,112 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from '@/stores/authStore.js';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js';
export const useContentItemsStore = defineStore('content-items', () => {
const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore();
const client = useClient();
const items = ref([]);
const isLoading = ref(false);
const isCreating = ref(false);
const error = ref(null);
const activeCount = computed(() =>
items.value.filter(item => item.status !== 'Approved' && item.status !== 'Published' && item.status !== 'Archived')
.length
);
async function fetchContentItems(filters = {}) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
items.value = [];
error.value = null;
return;
}
isLoading.value = true;
error.value = null;
try {
const response = await client.get('/api/content-items', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
clientId: filters.clientId,
projectId: filters.projectId,
},
});
items.value = response.data ?? [];
} catch (fetchError) {
console.error('Failed to fetch content items:', fetchError);
items.value = [];
error.value = 'Failed to load content items.';
} finally {
isLoading.value = false;
}
}
async function createContentItem(payload) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to create a content item.');
}
if (isCreating.value) {
throw new Error('A content item creation request is already in progress.');
}
isCreating.value = true;
error.value = null;
try {
const response = await client.post('/api/content-items', {
...payload,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
items.value = [response.data, ...items.value];
}
return response.data;
} catch (createError) {
console.error('Failed to create content item:', createError);
error.value = 'Failed to create content item.';
throw createError;
} finally {
isCreating.value = false;
}
}
async function fetchContentItem(id) {
const response = await client.get(`/api/content-items/${id}`);
return response.data;
}
watch(
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
async ([isAuthenticated, workspaceId]) => {
if (!isAuthenticated || !workspaceId) {
items.value = [];
error.value = null;
return;
}
await fetchContentItems();
},
{ immediate: true }
);
return {
items,
isLoading,
isCreating,
error,
activeCount,
fetchContentItems,
fetchContentItem,
createContentItem,
};
});

View File

@@ -1,86 +0,0 @@
import {useClient} from '@/plugins/api.js';
import {useAuthStore} from '@/stores/authStore.js';
import {useSessionStorage} from '@vueuse/core';
import {defineStore} from 'pinia';
import {computed, watch} from 'vue';
import {useRouter} from 'vue-router';
export const useCreatorProfileStore = defineStore(
'creator-profile',
() => {
const router = useRouter();
const authStore = useAuthStore();
watch(
() => authStore.isAuthenticated,
async (newValue) => {
if (newValue) {
await fetchCreatorProfile();
if (value.value && value.value.name !== undefined) {
await router.push(`/@${value.value.slug}`);
} else {
await router.push('/');
}
} else if (!authStore.isRefreshing) {
value.value = undefined;
}
}
);
const value = useSessionStorage(
'creator-profile',
{},
{
writeDefaults: false,
storage: window.sessionStorage,
serializer: {
read: (value) => value ? JSON.parse(value) : undefined,
write: (value) => value ? JSON.stringify(value) : undefined
}
}
);
const hasCreator = computed(
() => value.value && Object.getOwnPropertyNames(value.value).length >= 1
);
const client = useClient();
async function fetchCreatorProfile() {
try {
const response = await client.get(`/api/creators/profile`);
value.value = response.data;
} catch (error) {
value.value = undefined;
}
}
async function removeCreatorPage() {
try {
await client.delete(`/api/creators/@${value.value.slug}`)
await fetchCreatorProfile();
}
catch(error) {
console.error(error);
}
}
async function restoreCreatorPage() {
try {
await client.put(`/api/creators/@${value.value.slug}/restore`, {})
await fetchCreatorProfile();
}
catch(error) {
console.error(error);
}
}
return {
creator: value,
hasCreator,
removeCreatorPage,
restoreCreatorPage,
fetchCreatorProfile
};
});

View File

@@ -1,15 +1,13 @@
import { defineStore } from 'pinia';
import { useSessionStorage } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import { i18n } from '@/plugins/i18n.js';
const ALLOWED_LOCALES = ['en', 'fr'];
const DEFAULT_LOCALE = 'fr';
const DEFAULT_LOCALE = 'en';
export const useLanguageStore = defineStore('language', () => {
const storedLocale = useSessionStorage('user-locale', DEFAULT_LOCALE);
// Get i18n instance (provided globally)
const { locale } = useI18n();
const locale = i18n.global.locale;
function sanitizeLocale(value) {
return ALLOWED_LOCALES.includes(value) ? value : DEFAULT_LOCALE;
@@ -18,15 +16,11 @@ export const useLanguageStore = defineStore('language', () => {
// Initialize locale with a sanitized value
const initial = sanitizeLocale(storedLocale.value);
storedLocale.value = initial;
if (locale) {
locale.value = initial;
}
locale.value = initial;
function setLocale(newLocale) {
const next = sanitizeLocale(newLocale);
if (locale) {
locale.value = next;
}
locale.value = next;
storedLocale.value = next;
}

View File

@@ -0,0 +1,89 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from '@/stores/authStore.js';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js';
export const useNotificationsStore = defineStore('notifications', () => {
const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore();
const client = useClient();
const items = ref([]);
const isLoading = ref(false);
const error = ref(null);
const unreadCount = computed(() =>
items.value.filter(item => !item.readAt).length
);
const recentItems = computed(() => items.value.slice(0, 6));
function reset() {
items.value = [];
error.value = null;
}
async function fetchNotifications() {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
reset();
return;
}
isLoading.value = true;
error.value = null;
try {
const response = await client.get('/api/notifications', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
},
});
items.value = response.data ?? [];
} catch (fetchError) {
console.error('Failed to fetch notifications:', fetchError);
items.value = [];
error.value = 'Failed to load notifications.';
} finally {
isLoading.value = false;
}
}
async function markAsRead(notificationId) {
try {
await client.post(`/api/notifications/${notificationId}/read`);
items.value = items.value.map(item =>
item.id === notificationId
? { ...item, readAt: item.readAt ?? new Date().toISOString() }
: item
);
} catch (markError) {
console.error('Failed to mark notification as read:', markError);
}
}
watch(
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
async ([isAuthenticated, workspaceId]) => {
if (!isAuthenticated || !workspaceId) {
reset();
return;
}
await fetchNotifications();
},
{ immediate: true }
);
return {
items,
recentItems,
unreadCount,
isLoading,
error,
reset,
fetchNotifications,
markAsRead,
};
});

View File

@@ -0,0 +1,99 @@
import { ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from '@/stores/authStore.js';
import { useWorkspaceStore } from '@/stores/workspaceStore.js';
import { useClient } from '@/plugins/api.js';
export const useProjectsStore = defineStore('projects', () => {
const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore();
const client = useClient();
const projects = ref([]);
const isLoading = ref(false);
const isCreating = ref(false);
const error = ref(null);
async function fetchProjects() {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
projects.value = [];
error.value = null;
return;
}
isLoading.value = true;
error.value = null;
try {
const response = await client.get('/api/projects', {
params: {
workspaceId: workspaceStore.activeWorkspaceId,
},
});
projects.value = response.data ?? [];
} catch (fetchError) {
console.error('Failed to fetch projects:', fetchError);
projects.value = [];
error.value = 'Failed to load projects.';
} finally {
isLoading.value = false;
}
}
async function createProject(payload) {
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
throw new Error('You must be authenticated to create a project.');
}
if (isCreating.value) {
throw new Error('A project creation request is already in progress.');
}
isCreating.value = true;
error.value = null;
try {
const response = await client.post('/api/projects', {
...payload,
workspaceId: workspaceStore.activeWorkspaceId,
});
if (response.data) {
projects.value = [...projects.value, response.data]
.sort((left, right) => left.name.localeCompare(right.name));
}
return response.data;
} catch (createError) {
console.error('Failed to create project:', createError);
error.value = 'Failed to create project.';
throw createError;
} finally {
isCreating.value = false;
}
}
watch(
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
async ([isAuthenticated, workspaceId]) => {
if (!isAuthenticated || !workspaceId) {
projects.value = [];
error.value = null;
return;
}
await fetchProjects();
},
{ immediate: true }
);
return {
projects,
isLoading,
isCreating,
error,
fetchProjects,
createProject,
};
});

View File

@@ -0,0 +1,49 @@
import { computed } from 'vue';
import { defineStore } from 'pinia';
import { useContentItemsStore } from '@/stores/contentItemsStore.js';
import { useProjectsStore } from '@/stores/projectsStore.js';
const stageByStatus = {
Draft: 'Draft',
'In internal review': 'Internal review',
'Changes requested internally': 'Internal changes requested',
'Internal changes in progress': 'Internal revision',
'Ready for client review': 'Ready for client review',
'In client review': 'Client review',
'Changes requested by client': 'Client changes requested',
'Client changes in progress': 'Client revision',
Approved: 'Approved',
Rejected: 'Rejected',
'Ready to publish': 'Ready to publish',
Published: 'Published',
Archived: 'Archived',
};
export const useReviewQueueStore = defineStore('review-queue', () => {
const contentItemsStore = useContentItemsStore();
const projectsStore = useProjectsStore();
const items = computed(() =>
contentItemsStore.items
.filter(item => item.status !== 'Draft' && item.status !== 'Published' && item.status !== 'Archived')
.map(item => {
const project = projectsStore.projects.find(candidate => candidate.id === item.projectId);
return {
id: item.id,
title: item.title,
projectName: project?.name ?? 'Unknown campaign',
stage: stageByStatus[item.status] ?? item.status,
status: item.status,
dueLabel: item.dueDate ? `Due ${new Date(item.dueDate).toLocaleDateString()}` : 'No due date',
};
})
);
const urgentItems = computed(() => items.value.slice(0, 5));
return {
items,
urgentItems,
};
});

View File

@@ -50,9 +50,15 @@ export const useUserProfileStore = defineStore(
const portraitUrl = computed(() => {
return value.value && value.value.portraitUrl
? value.value.portraitUrl
: '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'
: null
})
const roles = computed(() => value.value?.userRoles ?? [])
const persona = computed(() => value.value?.persona ?? null)
const authorizedWorkspaceIds = computed(() => value.value?.authorizedWorkspaceIds ?? [])
const authorizedClientIds = computed(() => value.value?.authorizedClientIds ?? [])
const authorizedProjectIds = computed(() => value.value?.authorizedProjectIds ?? [])
async function fetchCurrentUserProfile() {
try {
const client = useClient()
@@ -153,7 +159,7 @@ export const useUserProfileStore = defineStore(
try {
const client = useClient()
const formData = new FormData();
formData.append('file', selectedFile)
formData.append('file', selectedFile, selectedFile.name || 'portrait.png')
const response = await client.post(
`/api/users/portrait`,
@@ -170,6 +176,11 @@ export const useUserProfileStore = defineStore(
alias,
fullname,
portraitUrl,
roles,
persona,
authorizedWorkspaceIds,
authorizedClientIds,
authorizedProjectIds,
changeFullname,
changeAlias,
changeBirthday,

View File

@@ -0,0 +1,208 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from '@/stores/authStore.js';
import { useClient } from '@/plugins/api.js';
export const useWorkspaceStore = defineStore('workspace', () => {
const authStore = useAuthStore();
const client = useClient();
const workspaces = ref([]);
const activeWorkspaceId = ref(null);
const isLoading = ref(false);
const isCreating = ref(false);
const invitesByWorkspace = ref({});
const membersByWorkspace = ref({});
const isInvitesLoading = ref(false);
const isMembersLoading = ref(false);
const isInviting = ref(false);
const error = ref(null);
const activeWorkspace = computed(() =>
workspaces.value.find(workspace => workspace.id === activeWorkspaceId.value) ?? null
);
async function fetchWorkspaces() {
if (!authStore.isAuthenticated) {
workspaces.value = [];
activeWorkspaceId.value = null;
error.value = null;
return;
}
isLoading.value = true;
error.value = null;
try {
const response = await client.get('/api/workspaces');
workspaces.value = response.data ?? [];
if (!workspaces.value.some(workspace => workspace.id === activeWorkspaceId.value)) {
activeWorkspaceId.value = workspaces.value[0]?.id ?? null;
}
} catch (fetchError) {
console.error('Failed to fetch workspaces:', fetchError);
workspaces.value = [];
activeWorkspaceId.value = null;
error.value = 'Failed to load workspaces.';
} finally {
isLoading.value = false;
}
}
async function createWorkspace(payload) {
if (!authStore.isAuthenticated) {
throw new Error('You must be authenticated to create a workspace.');
}
if (isCreating.value) {
throw new Error('A workspace creation request is already in progress.');
}
isCreating.value = true;
error.value = null;
try {
const response = await client.post('/api/workspaces', payload);
if (response.data) {
workspaces.value = [...workspaces.value, response.data]
.sort((left, right) => left.name.localeCompare(right.name));
activeWorkspaceId.value = response.data.id;
try {
await client.post('/api/clients', {
workspaceId: response.data.id,
name: response.data.name,
});
} catch (hiddenClientError) {
console.error('Failed to provision operational client for workspace:', hiddenClientError);
}
}
return response.data;
} catch (createError) {
console.error('Failed to create workspace:', createError);
error.value = 'Failed to create workspace.';
throw createError;
} finally {
isCreating.value = false;
}
}
function setActiveWorkspace(workspaceId) {
if (workspaces.value.some(workspace => workspace.id === workspaceId)) {
activeWorkspaceId.value = workspaceId;
}
}
async function fetchInvites(workspaceId = activeWorkspaceId.value) {
if (!authStore.isAuthenticated || !workspaceId) {
invitesByWorkspace.value = {};
return [];
}
isInvitesLoading.value = true;
try {
const response = await client.get(`/api/workspaces/${workspaceId}/invites`);
invitesByWorkspace.value = {
...invitesByWorkspace.value,
[workspaceId]: response.data ?? [],
};
return invitesByWorkspace.value[workspaceId];
} catch (fetchError) {
console.error('Failed to fetch workspace invites:', fetchError);
throw fetchError;
} finally {
isInvitesLoading.value = false;
}
}
async function fetchMembers(workspaceId = activeWorkspaceId.value) {
if (!authStore.isAuthenticated || !workspaceId) {
membersByWorkspace.value = {};
return [];
}
isMembersLoading.value = true;
try {
const response = await client.get(`/api/workspaces/${workspaceId}/members`);
membersByWorkspace.value = {
...membersByWorkspace.value,
[workspaceId]: response.data ?? [],
};
return membersByWorkspace.value[workspaceId];
} catch (fetchError) {
console.error('Failed to fetch workspace members:', fetchError);
throw fetchError;
} finally {
isMembersLoading.value = false;
}
}
async function inviteMember(payload) {
if (!authStore.isAuthenticated || !activeWorkspaceId.value) {
throw new Error('You must be authenticated to invite a workspace member.');
}
if (isInviting.value) {
throw new Error('A workspace invite request is already in progress.');
}
isInviting.value = true;
try {
const response = await client.post(`/api/workspaces/${activeWorkspaceId.value}/invites`, payload);
invitesByWorkspace.value = {
...invitesByWorkspace.value,
[activeWorkspaceId.value]: [response.data, ...(invitesByWorkspace.value[activeWorkspaceId.value] ?? [])],
};
return response.data;
} catch (inviteError) {
console.error('Failed to create workspace invite:', inviteError);
throw inviteError;
} finally {
isInviting.value = false;
}
}
watch(
() => authStore.isAuthenticated,
async isAuthenticated => {
if (!isAuthenticated) {
workspaces.value = [];
activeWorkspaceId.value = null;
error.value = null;
return;
}
await fetchWorkspaces();
},
{ immediate: true }
);
return {
workspaces,
activeWorkspaceId,
activeWorkspace,
isLoading,
isCreating,
invitesByWorkspace,
membersByWorkspace,
isInvitesLoading,
isMembersLoading,
isInviting,
error,
fetchWorkspaces,
createWorkspace,
fetchInvites,
fetchMembers,
inviteMember,
setActiveWorkspace,
};
});