refactor: organize frontend by feature
This commit is contained in:
@@ -1,300 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { formatDuration } from '@/internal_time_ago.js';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const clientApi = useClient();
|
||||
const router = useRouter();
|
||||
|
||||
const isRefreshing = ref(false);
|
||||
let refreshPromise = null;
|
||||
|
||||
const accessToken = useSessionStorage('auth-accessToken', undefined);
|
||||
const refreshToken = useSessionStorage('auth-refreshToken', undefined);
|
||||
const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
|
||||
serializer: {
|
||||
read: v => (v ? JSON.parse(v) : null),
|
||||
write: v => (v ? JSON.stringify(v) : null),
|
||||
},
|
||||
});
|
||||
|
||||
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) {
|
||||
throw new Error('Invalid token data');
|
||||
}
|
||||
accessToken.value = data.accessToken;
|
||||
refreshToken.value = data.refreshToken;
|
||||
const claims = getClaimsFromToken(data.accessToken);
|
||||
tokenClaims.value = claims;
|
||||
console.log('Tokens updated, user ID:', claims?.sub);
|
||||
}
|
||||
|
||||
function cleanTokens() {
|
||||
console.log('cleanTokens called - clearing stored tokens');
|
||||
accessToken.value = undefined;
|
||||
refreshToken.value = undefined;
|
||||
tokenClaims.value = null;
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
cleanTokens();
|
||||
await router.push('/');
|
||||
}
|
||||
|
||||
async function login(email, password) {
|
||||
console.log('login called with email:', email);
|
||||
if (!email || !password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await clientApi.post('api/users/login', {
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||
throw new Error('Invalid login response');
|
||||
}
|
||||
|
||||
updateTokens(response.data);
|
||||
console.log('login successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
cleanTokens();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithGoogle(accessTokenParam) {
|
||||
console.log('loginWithGoogle called');
|
||||
if (!accessTokenParam) {
|
||||
throw new Error('Google access token is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await clientApi.post('api/users/login-with-google', {
|
||||
token: accessTokenParam,
|
||||
});
|
||||
|
||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||
throw new Error('Invalid Google login response');
|
||||
}
|
||||
|
||||
updateTokens(response.data);
|
||||
console.log('Google login successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Google login failed:', error);
|
||||
cleanTokens();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithFacebook(authResponse) {
|
||||
console.log('loginWithFacebook called');
|
||||
if (!authResponse?.accessToken) {
|
||||
throw new Error('Facebook access token is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await clientApi.post('api/users/login-with-facebook', {
|
||||
token: authResponse.accessToken,
|
||||
});
|
||||
|
||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||
throw new Error('Invalid Facebook login response');
|
||||
}
|
||||
|
||||
updateTokens(response.data);
|
||||
console.log('Facebook login successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Facebook login failed:', error);
|
||||
cleanTokens();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
console.log('refresh called');
|
||||
|
||||
if (!refreshToken.value) {
|
||||
cleanTokens(); // Clear tokens first
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
if (isRefreshing.value && refreshPromise) {
|
||||
console.log('Already refreshing, returning existing refreshPromise');
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing.value = true;
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
console.log('Sending refresh request...');
|
||||
|
||||
const response = await clientApi.post('api/users/refresh', {
|
||||
refreshToken: refreshToken.value,
|
||||
});
|
||||
|
||||
if (!response.data?.accessToken || !response.data?.refreshToken) {
|
||||
throw new Error('Invalid refresh response');
|
||||
}
|
||||
|
||||
updateTokens({
|
||||
accessToken: response.data.accessToken,
|
||||
refreshToken: response.data.refreshToken,
|
||||
});
|
||||
|
||||
console.log('Token refresh successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
cleanTokens();
|
||||
|
||||
const currentRoute = router.currentRoute.value;
|
||||
const returnUrl = currentRoute.fullPath;
|
||||
|
||||
// Handle navigation
|
||||
router
|
||||
.push({
|
||||
name: 'login',
|
||||
query: { returnUrl },
|
||||
})
|
||||
.catch(navError => {
|
||||
console.error('Navigation error after token refresh failure:', navError);
|
||||
});
|
||||
|
||||
throw error; // Re-throw to notify callers
|
||||
}
|
||||
})();
|
||||
|
||||
return await refreshPromise;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
// Ensure these are always reset, even if an error is thrown
|
||||
isRefreshing.value = false;
|
||||
refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getClaimsFromToken(token) {
|
||||
if (!token) return null;
|
||||
try {
|
||||
return jwtDecode(token);
|
||||
} catch (error) {
|
||||
console.error('Failed to decode token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isTokenExpiringSoon(token) {
|
||||
if (!token) {
|
||||
console.log('No token provided, considered expiring soon');
|
||||
return true;
|
||||
}
|
||||
|
||||
const claims = getClaimsFromToken(token);
|
||||
if (!claims || !claims.exp) {
|
||||
console.log('No valid claims found, considered expiring soon');
|
||||
return true;
|
||||
}
|
||||
|
||||
const expirationTime = claims.exp * 1000; // Convert to milliseconds
|
||||
const currentTime = Date.now();
|
||||
const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
|
||||
|
||||
// Calculate time remaining (can be negative if already expired)
|
||||
const timeRemainingMs = expirationTime - currentTime;
|
||||
|
||||
// Token is expiring soon if less than 2 minutes remaining or already expired
|
||||
const isExpiring = timeRemainingMs < fiveMinutesInMs;
|
||||
|
||||
// Determine the sign for display purposes
|
||||
const formattedTimeRemaining =
|
||||
timeRemainingMs < 0 ? `-${formatDuration(Math.abs(timeRemainingMs))}` : formatDuration(timeRemainingMs);
|
||||
|
||||
if (isExpiring) {
|
||||
console.log(`Token expiration check; is token expired: ${isExpiring}`, {
|
||||
expirationTime: new Date(expirationTime).toLocaleString(),
|
||||
currentTime: new Date(currentTime).toLocaleString(),
|
||||
timeRemaining: formattedTimeRemaining,
|
||||
});
|
||||
}
|
||||
|
||||
return isExpiring;
|
||||
}
|
||||
|
||||
async function changePassword(newPassword) {
|
||||
console.log('changePassword called');
|
||||
if (!isAuthenticated.value) {
|
||||
throw new Error('User must be authenticated to change password');
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
throw new Error('New password is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await clientApi.post('api/users/set-password', {
|
||||
newPassword,
|
||||
});
|
||||
|
||||
console.log('Password changed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Password change failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
loginWithFacebook,
|
||||
logout,
|
||||
refresh,
|
||||
isTokenExpiringSoon,
|
||||
changePassword,
|
||||
};
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,182 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,255 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
import {computed, watch} from 'vue'
|
||||
import {defineStore} from 'pinia'
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
|
||||
export const useUserProfileStore = defineStore(
|
||||
'user-profile',
|
||||
() => {
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const authWatcher = watch(
|
||||
() => authStore.isAuthenticated,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
await fetchCurrentUserProfile()
|
||||
} else if (!authStore.isRefreshing) {
|
||||
value.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const value = useSessionStorage(
|
||||
'user-profile',
|
||||
{},
|
||||
{writeDefaults: false})
|
||||
|
||||
const fullname = computed(() => {
|
||||
if (value.value) {
|
||||
const {firstname, lastname} = value.value;
|
||||
|
||||
if (firstname && lastname) {
|
||||
return `${lastname}, ${firstname}`;
|
||||
} else if (firstname) {
|
||||
return firstname;
|
||||
} else if (lastname) {
|
||||
return lastname;
|
||||
}
|
||||
}
|
||||
return 'n/a';
|
||||
})
|
||||
|
||||
const alias = computed(() => {
|
||||
if (value.value) {
|
||||
return value.value.alias || `${value.value.firstname || ''} ${value.value.lastname || ''}`.trim() || 'Anonyme'
|
||||
}
|
||||
return 'Anonyme';
|
||||
})
|
||||
|
||||
const portraitUrl = computed(() => {
|
||||
return value.value && value.value.portraitUrl
|
||||
? value.value.portraitUrl
|
||||
: 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()
|
||||
const userResponse = await client.get("/api/users/profile");
|
||||
value.value = userResponse.data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeFullname(firstname, lastname) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/fullname`,
|
||||
{
|
||||
firstname: firstname,
|
||||
lastname: lastname
|
||||
})
|
||||
value.value.firstname = firstname;
|
||||
value.value.lastname = lastname;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeAlias(alias) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/alias`,
|
||||
{
|
||||
alias: alias
|
||||
})
|
||||
value.value.alias = alias;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeBirthday(birthdate) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/birthdate`,
|
||||
{
|
||||
birthdate: birthdate
|
||||
})
|
||||
value.value.birthDate = birthdate;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changePhone(phoneNumber) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/phone`,
|
||||
{
|
||||
phoneNumber: phoneNumber
|
||||
})
|
||||
value.value.phoneNumber = phoneNumber;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeEmail(email) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/email`,
|
||||
{
|
||||
email: email
|
||||
})
|
||||
value.value.email = email;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeAddress(address) {
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/address`,
|
||||
{
|
||||
address: address
|
||||
})
|
||||
value.value.address = address;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function changePortrait(selectedFile) {
|
||||
try {
|
||||
const client = useClient()
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile, selectedFile.name || 'portrait.png')
|
||||
|
||||
const response = await client.post(
|
||||
`/api/users/portrait`,
|
||||
formData)
|
||||
|
||||
value.value.portraitUrl = `${response.data.blobUrl}?${Date.now()}` // the Date.now() is for cache-busting
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user: value,
|
||||
alias,
|
||||
fullname,
|
||||
portraitUrl,
|
||||
roles,
|
||||
persona,
|
||||
authorizedWorkspaceIds,
|
||||
authorizedClientIds,
|
||||
authorizedProjectIds,
|
||||
changeFullname,
|
||||
changeAlias,
|
||||
changeBirthday,
|
||||
changePhone,
|
||||
changeEmail,
|
||||
changeAddress,
|
||||
changePortrait
|
||||
}
|
||||
})
|
||||
@@ -1,208 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user