This commit is contained in:
2026-08-28 17:31:02 +02:00
commit 2b30e8bd39
694 changed files with 49243 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
const CMS_AUTH_STORAGE_KEY = 'cms-auth';
export const useAuthStore = defineStore('auth', () => {
const isSignedIn = ref(localStorage.getItem(CMS_AUTH_STORAGE_KEY) === '1');
const markSignedIn = () => {
localStorage.setItem(CMS_AUTH_STORAGE_KEY, '1');
isSignedIn.value = true;
};
const clearSession = () => {
localStorage.removeItem(CMS_AUTH_STORAGE_KEY);
isSignedIn.value = false;
};
const login = async (password: string) => {
await api.post('/auth/login', { password });
markSignedIn();
};
return {
isSignedIn,
login,
clearSession
};
});
+57
View File
@@ -0,0 +1,57 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
import type { CreateOrUpdateCategoryPayload } from '@/types/category/CreateOrUpdateCategoryPayload';
import type { Category } from '@/types/product/Category';
export const useCategoriesStore = defineStore('categories', () => {
const categories = ref<Category[]>([]);
const upsertCategory = (data: Category): void => {
const idx = categories.value.findIndex(c => c.id === data.id);
if (idx !== -1) {
categories.value[idx] = data;
} else {
categories.value.push(data);
}
categories.value.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
};
const fetchAll = async (): Promise<void> => {
const { data } = await api.get<Category[]>('/categories');
categories.value = data;
};
const createCategory = async (payload: CreateOrUpdateCategoryPayload): Promise<Category> => {
const { data } = await api.post<Category>('/categories', payload);
upsertCategory(data);
return data;
};
const updateCategory = async (id: string, payload: CreateOrUpdateCategoryPayload): Promise<Category> => {
const { data } = await api.patch<Category>(`/categories/${id}`, payload);
upsertCategory(data);
return data;
};
const removeCategory = async (id: string): Promise<void> => {
await api.delete(`/categories/${id}`);
categories.value = categories.value.filter(c => c.id !== id);
};
return {
categories,
fetchAll,
createCategory,
updateCategory,
removeCategory
};
});
+46
View File
@@ -0,0 +1,46 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
const STORAGE_KEY = 'cms-color-mode';
const resolveInitialDark = (): boolean => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'dark') {
return true;
}
if (saved === 'light') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch {
return false;
}
};
export const useColorSchemeStore = defineStore('colorScheme', () => {
const isDark = ref(false);
const initColorScheme = (): void => {
isDark.value = resolveInitialDark();
document.documentElement.classList.toggle('dark', isDark.value);
};
const toggleDarkMode = (): void => {
isDark.value = !isDark.value;
document.documentElement.classList.toggle('dark', isDark.value);
localStorage.setItem(STORAGE_KEY, isDark.value ? 'dark' : 'light');
};
return {
isDark,
initColorScheme,
toggleDarkMode
};
});
+148
View File
@@ -0,0 +1,148 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
import type { PaginatedResponse } from '@/types/PaginatedResponse';
import type { DigitalStockItem } from '@/types/product/DigitalStockItem';
import type { DigitalStockListQuery } from '@/types/product/DigitalStockListQuery';
import { useProductsStore } from './products';
export const useDigitalStockStore = defineStore('digitalStock', () => {
const items = ref<DigitalStockItem[]>([]);
const resetList = (): void => {
items.value = [];
};
const upsertItemInList = (item: DigitalStockItem): void => {
const idx = items.value.findIndex(row => row.id === item.id);
if (idx === -1) {
items.value.push(item);
} else {
items.value[idx] = item;
}
};
const fetchList = async (
productId: string,
variantId: string,
{ page, limit, hideSold }: DigitalStockListQuery
): Promise<PaginatedResponse<DigitalStockItem>> => {
const { data } = await api.get<PaginatedResponse<DigitalStockItem>>(
`/products/${productId}/variants/${variantId}/digital-stock-items`,
{ params: { page, limit, hideSold: hideSold ? 1 : 0 } }
);
items.value = data.items;
return data;
};
const addItem = async (productId: string, variantId: string, content: string): Promise<DigitalStockItem> => {
const { data } = await api.post<DigitalStockItem>(
`/products/${productId}/variants/${variantId}/digital-stock-items`,
{ content }
);
const { adjustVariantStockAvailable } = useProductsStore();
adjustVariantStockAvailable(productId, variantId, 1);
return data;
};
const updateItem = async (
productId: string,
variantId: string,
itemId: string,
content: string
): Promise<DigitalStockItem> => {
const { data } = await api.patch<DigitalStockItem>(
`/products/${productId}/variants/${variantId}/digital-stock-items/${itemId}`,
{ content }
);
upsertItemInList(data);
return data;
};
const removeItem = async (productId: string, variantId: string, itemId: string, isSold: boolean): Promise<void> => {
await api.delete(`/products/${productId}/variants/${variantId}/digital-stock-items/${itemId}`);
if (!isSold) {
const { adjustVariantStockAvailable } = useProductsStore();
adjustVariantStockAvailable(productId, variantId, -1);
}
};
const uploadAttachment = async (
productId: string,
variantId: string,
itemId: string,
file: File
): Promise<DigitalStockItem> => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post<DigitalStockItem>(
`/products/${productId}/variants/${variantId}/digital-stock-items/${itemId}/attachments`,
formData
);
upsertItemInList(data);
return data;
};
const removeAttachment = async (
productId: string,
variantId: string,
itemId: string,
attachmentId: string
): Promise<DigitalStockItem> => {
const { data } = await api.delete<DigitalStockItem>(
`/products/${productId}/variants/${variantId}/digital-stock-items/${itemId}/attachments/${attachmentId}`
);
upsertItemInList(data);
return data;
};
const downloadAttachment = async (
productId: string,
variantId: string,
itemId: string,
attachmentId: string,
filename: string
): Promise<void> => {
const { data } = await api.get<Blob>(
`/products/${productId}/variants/${variantId}/digital-stock-items/${itemId}/attachments/${attachmentId}/download`,
{ responseType: 'blob' }
);
const url = URL.createObjectURL(data);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
};
return {
items,
resetList,
fetchList,
addItem,
updateItem,
removeItem,
uploadAttachment,
removeAttachment,
downloadAttachment
};
});
+67
View File
@@ -0,0 +1,67 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
import type { CreateOrUpdateDiscountCodePayload } from '@/types/discountCode/CreateOrUpdateDiscountCodePayload';
import type { DiscountCode } from '@/types/discountCode/DiscountCode';
export const useDiscountCodesStore = defineStore('discountCodes', () => {
const discountCodes = ref<DiscountCode[]>([]);
const upsertDiscountCode = (data: DiscountCode): void => {
const idx = discountCodes.value.findIndex(c => c.id === data.id);
if (idx !== -1) {
discountCodes.value[idx] = data;
} else {
discountCodes.value.unshift(data);
}
};
const fetchAll = async (): Promise<void> => {
const { data } = await api.get<DiscountCode[]>('/discount-codes');
discountCodes.value = data;
};
const fetchDiscountCodeById = async (id: string): Promise<DiscountCode> => {
const { data } = await api.get<DiscountCode>(`/discount-codes/${id}`);
upsertDiscountCode(data);
return data;
};
const createDiscountCode = async (payload: CreateOrUpdateDiscountCodePayload): Promise<DiscountCode> => {
const { data } = await api.post<DiscountCode>('/discount-codes', payload);
upsertDiscountCode(data);
return data;
};
const updateDiscountCode = async (
id: string,
payload: CreateOrUpdateDiscountCodePayload
): Promise<DiscountCode> => {
const { data } = await api.patch<DiscountCode>(`/discount-codes/${id}`, payload);
upsertDiscountCode(data);
return data;
};
const removeDiscountCode = async (id: string): Promise<void> => {
await api.delete(`/discount-codes/${id}`);
discountCodes.value = discountCodes.value.filter(c => c.id !== id);
};
return {
discountCodes,
fetchAll,
fetchDiscountCodeById,
createDiscountCode,
updateDiscountCode,
removeDiscountCode
};
});
+39
View File
@@ -0,0 +1,39 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
import type { MoneroWalletRevealSeedPayload } from '@/types/moneroWallet/MoneroWalletRevealSeedPayload';
import type { MoneroWalletRevealSeedResult } from '@/types/moneroWallet/MoneroWalletRevealSeedResult';
import type { MoneroWalletStatus } from '@/types/moneroWallet/MoneroWalletStatus';
import type { MoneroWalletWithdrawPayload } from '@/types/moneroWallet/MoneroWalletWithdrawPayload';
import type { MoneroWalletWithdrawResult } from '@/types/moneroWallet/MoneroWalletWithdrawResult';
export const useMoneroWalletStore = defineStore('moneroWallet', () => {
const status = ref<MoneroWalletStatus | null>(null);
const fetchStatus = async (): Promise<MoneroWalletStatus> => {
const { data } = await api.get<MoneroWalletStatus>('/monero-wallet');
status.value = data;
return data;
};
const withdrawAll = async (payload: MoneroWalletWithdrawPayload): Promise<MoneroWalletWithdrawResult> => {
const { data } = await api.post<MoneroWalletWithdrawResult>('/monero-wallet/withdraw', payload);
return data;
};
const revealSeed = async (payload: MoneroWalletRevealSeedPayload): Promise<MoneroWalletRevealSeedResult> => {
const { data } = await api.post<MoneroWalletRevealSeedResult>('/monero-wallet/reveal-seed', payload);
return data;
};
return {
status,
fetchStatus,
withdrawAll,
revealSeed
};
});
+104
View File
@@ -0,0 +1,104 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { ROUTE_NAMES } from '@/consts/routeNames';
import { api } from '@/plugins/axios';
import type { PaginatedResponse } from '@/types/PaginatedResponse';
import type { OrderExtended } from '@/types/order/OrderExtended';
import type { OrderListItem } from '@/types/order/OrderListItem';
import type { OrderMessage } from '@/types/order/OrderMessage';
import type { SetDeliveryCostPayload } from '@/types/order/SetDeliveryCostPayload';
export const useOrdersStore = defineStore('orders', () => {
const route = useRoute();
const orderList = ref<OrderListItem[]>([]);
const currentOrder = ref<OrderExtended | null>(null);
const currentOrderId = computed<string | null>(() => {
if (route.name !== ROUTE_NAMES.OrderDetail) {
return null;
}
const id = route.params.id;
return typeof id === 'string' && id ? id : null;
});
const fetchList = async ({
page,
limit
}: {
page: number;
limit: number;
}): Promise<PaginatedResponse<OrderListItem>> => {
const { data } = await api.get<PaginatedResponse<OrderListItem>>('/orders', {
params: { page, limit }
});
orderList.value = data.items;
return data;
};
const fetchById = async (id: string): Promise<OrderExtended> => {
const { data } = await api.get<OrderExtended>(`/orders/${id}`);
currentOrder.value = data;
return data;
};
const markChatRead = async (id: string): Promise<void> => {
await api.post(`/orders/${id}/messages/mark-read`);
};
const sendMessage = async (id: string, body: string): Promise<OrderMessage[]> => {
const { data } = await api.post<OrderMessage[]>(`/orders/${id}/messages`, { body });
if (currentOrder.value?.id === id) {
currentOrder.value = { ...currentOrder.value, messages: data };
}
return data;
};
const deleteMessage = async (orderId: string, messageId: string): Promise<OrderMessage[]> => {
const { data } = await api.delete<OrderMessage[]>(`/orders/${orderId}/messages/${messageId}`);
if (currentOrder.value?.id === orderId) {
currentOrder.value = { ...currentOrder.value, messages: data };
}
return data;
};
const setDeliveryCost = async (id: string, payload: SetDeliveryCostPayload): Promise<OrderExtended> => {
const { data } = await api.post<OrderExtended>(`/orders/${id}/delivery-cost`, payload);
currentOrder.value = data;
return data;
};
const fulfillManualLine = async (orderId: string, lineId: string): Promise<OrderExtended> => {
const { data } = await api.post<OrderExtended>(`/orders/${orderId}/lines/${lineId}/fulfill`);
currentOrder.value = data;
return data;
};
return {
orderList,
currentOrder,
currentOrderId,
fetchList,
fetchById,
markChatRead,
sendMessage,
deleteMessage,
setDeliveryCost,
fulfillManualLine
};
});
+335
View File
@@ -0,0 +1,335 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import type { FormRules } from 'element-plus';
import { config } from '@/config';
import { ROUTE_NAMES } from '@/consts/routeNames';
import { api } from '@/plugins/axios';
import type { PaginatedResponse } from '@/types/PaginatedResponse';
import type { ProductWithVariantsExtended } from '@/types/product/ProductWithVariantsExtended';
import { DeliveryMode } from '@/types/product/DeliveryMode';
import type { UpdateProductPayload } from '@/types/product/UpdateProductPayload';
import type { ProductVariantPayload } from '@/types/product/ProductVariantPayload';
import type { ProductVariant } from '@/types/product/ProductVariant';
import type { ProductVariantExtended } from '@/types/product/ProductVariantExtended';
import { compareProductVariants } from '@/utils/product/compareProductVariants';
import { formatDeliveryMode } from '@/utils/product/formatDeliveryMode';
import { getProductTitle } from '@/utils/product/getProductTitle';
const {
validation: { productTitleMaxLength: validationProductTitleMaxLength }
} = config;
export const useProductsStore = defineStore('products', () => {
const route = useRoute();
const products = ref<ProductWithVariantsExtended[]>([]);
const currentProductId = computed<string | null>(() => {
if (route.name === ROUTE_NAMES.ProductDetail) {
const id = route.params.id;
return typeof id === 'string' && id ? id : null;
}
if (route.name === ROUTE_NAMES.ProductVariantDetail) {
const id = route.params.productId;
return typeof id === 'string' && id ? id : null;
}
return null;
});
const currentVariantId = computed<string | null>(() => {
if (route.name !== ROUTE_NAMES.ProductVariantDetail) {
return null;
}
const id = route.params.variantId;
return typeof id === 'string' && id ? id : null;
});
const currentProduct = computed<ProductWithVariantsExtended | null>(() => {
if (!currentProductId.value) {
return null;
}
return products.value.find(product => product.id === currentProductId.value) ?? null;
});
const currentVariant = computed<ProductVariantExtended | null>(() => {
if (!currentVariantId.value) {
return null;
}
return currentProduct.value?.variants.find(variant => variant.id === currentVariantId.value) ?? null;
});
const currentDeliveryModeLabel = computed(() =>
currentProduct.value ? formatDeliveryMode(currentProduct.value.deliveryMode) : null
);
const currentProductDisplayTitle = computed(() =>
currentProduct.value ? getProductTitle(currentProduct.value.title) : null
);
const currentProductIsManual = computed(
() => currentProduct.value?.deliveryMode === DeliveryMode.Manual
);
const currentProductIsAuto = computed(
() => currentProduct.value?.deliveryMode === DeliveryMode.Auto
);
const canDeleteCurrentVariant = computed(() => (currentProduct.value?.variants.length ?? 0) > 1);
const createOrEditCurrentProductVariantFormRules = computed<FormRules>(() => ({
title: [
{ required: true, message: 'Required', trigger: 'blur' },
{
max: validationProductTitleMaxLength,
message: `At most ${validationProductTitleMaxLength} characters`,
trigger: 'blur'
}
],
price: [{ required: true, message: 'Required', trigger: 'change' }],
...(currentProductIsManual.value
? {
stockQuantity: [
{ required: true, message: 'Stock quantity is required', trigger: 'change' },
{ type: 'integer', message: 'Stock quantity must be a whole number', trigger: 'change' }
]
}
: {}),
sortOrder: [
{ required: true, message: 'Sort order is required', trigger: 'change' },
{ type: 'integer', message: 'Sort order must be a whole number', trigger: 'change' }
]
}));
const upsertProduct = (data: ProductWithVariantsExtended): void => {
const idx = products.value.findIndex(r => r.id === data.id);
if (idx !== -1) {
products.value[idx] = data;
} else {
products.value.unshift(data);
}
};
const upsertProductVariant = (productId: string, variant: ProductVariantExtended): void => {
const product = products.value.find(p => p.id === productId);
if (!product) {
return;
}
const idx = product.variants.findIndex(v => v.id === variant.id);
if (idx === -1) {
product.variants.push(variant);
} else {
product.variants[idx] = variant;
}
product.variants.sort(compareProductVariants);
};
const adjustVariantStockAvailable = (productId: string, variantId: string, delta: number): void => {
const product = products.value.find(p => p.id === productId);
const variant = product?.variants.find(v => v.id === variantId);
if (variant) {
variant.stockAvailable = Math.max(0, variant.stockAvailable + delta);
}
};
const fetchProducts = async ({
page,
limit,
search = ''
}: {
page: number;
limit: number;
search?: string;
}): Promise<PaginatedResponse<ProductWithVariantsExtended>> => {
const { data } = await api.get<PaginatedResponse<ProductWithVariantsExtended>>('/products', {
params: { page, limit, search }
});
return data;
};
const fetchProductVariants = async ({
search = '',
page = 1,
limit = 20
}: {
search?: string;
page?: number;
limit?: number;
} = {}): Promise<PaginatedResponse<ProductVariant>> => {
const { data } = await api.get<PaginatedResponse<ProductVariant>>('/products/variants', {
params: { search, page, limit }
});
return data;
};
const fetchProductById = async (id: string): Promise<ProductWithVariantsExtended> => {
const { data } = await api.get<ProductWithVariantsExtended>(`/products/${id}`);
upsertProduct(data);
return data;
};
const createDraftProduct = async (deliveryMode: DeliveryMode): Promise<ProductWithVariantsExtended> => {
const { data } = await api.post<ProductWithVariantsExtended>('/products', { deliveryMode });
upsertProduct(data);
return data;
};
const updateProduct = async (id: string, payload: UpdateProductPayload): Promise<ProductWithVariantsExtended> => {
const { data } = await api.patch<ProductWithVariantsExtended>(`/products/${id}`, payload);
upsertProduct(data);
return data;
};
const createProductVariant = async (
productId: string,
payload: ProductVariantPayload
): Promise<ProductVariantExtended> => {
const { data } = await api.post<ProductVariantExtended>(`/products/${productId}/variants`, payload);
upsertProductVariant(productId, data);
return data;
};
const updateProductVariant = async (
productId: string,
variantId: string,
payload: ProductVariantPayload
): Promise<ProductVariantExtended> => {
const { data } = await api.patch<ProductVariantExtended>(
`/products/${productId}/variants/${variantId}`,
payload
);
upsertProductVariant(productId, data);
return data;
};
const deleteProductVariant = async (productId: string, variantId: string): Promise<void> => {
await api.delete(`/products/${productId}/variants/${variantId}`);
const product = products.value.find(p => p.id === productId);
if (product) {
product.variants = product.variants.filter(v => v.id !== variantId);
}
};
const uploadVariantImage = async (
productId: string,
variantId: string,
file: File
): Promise<ProductVariantExtended> => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post<ProductVariantExtended>(
`/products/${productId}/variants/${variantId}/images`,
formData
);
upsertProductVariant(productId, data);
return data;
};
const removeVariantImage = async (
productId: string,
variantId: string,
imageId: string
): Promise<ProductVariantExtended> => {
const { data } = await api.delete<ProductVariantExtended>(
`/products/${productId}/variants/${variantId}/images/${imageId}`
);
upsertProductVariant(productId, data);
return data;
};
const setVariantImageThumbnail = async (
productId: string,
variantId: string,
imageId: string
): Promise<ProductVariantExtended> => {
const { data } = await api.patch<ProductVariantExtended>(
`/products/${productId}/variants/${variantId}/images/${imageId}/set-thumbnail`
);
upsertProductVariant(productId, data);
return data;
};
const reorderVariantImages = async (
productId: string,
variantId: string,
imageIds: string[]
): Promise<ProductVariantExtended> => {
const { data } = await api.patch<ProductVariantExtended>(
`/products/${productId}/variants/${variantId}/images/reorder`,
{ imageIds }
);
upsertProductVariant(productId, data);
return data;
};
const deleteProduct = async (id: string): Promise<void> => {
await api.delete(`/products/${id}`);
products.value = products.value.filter(p => p.id !== id);
};
return {
products,
currentProduct,
currentVariant,
currentProductId,
currentVariantId,
currentDeliveryModeLabel,
currentProductDisplayTitle,
currentProductIsManual,
currentProductIsAuto,
canDeleteCurrentVariant,
createOrEditCurrentProductVariantFormRules,
fetchProducts,
fetchProductVariants,
fetchProductById,
adjustVariantStockAvailable,
createDraftProduct,
updateProduct,
createProductVariant,
updateProductVariant,
deleteProductVariant,
uploadVariantImage,
removeVariantImage,
setVariantImageThumbnail,
reorderVariantImages,
deleteProduct
};
});
+87
View File
@@ -0,0 +1,87 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { api } from '@/plugins/axios';
import type { ConnectSimplexNotificationsPayload } from '@/types/shopSettings/ConnectSimplexNotificationsPayload';
import type { ShopSettings } from '@/types/shopSettings/ShopSettings';
import type { UpdateNotificationsPayload } from '@/types/shopSettings/UpdateNotificationsPayload';
import type { UpdateShippingNotePayload } from '@/types/shopSettings/UpdateShippingNotePayload';
import type { UpdateSimplexLinkPayload } from '@/types/shopSettings/UpdateSimplexLinkPayload';
export const useShopSettingsStore = defineStore('shopSettings', () => {
const settings = ref<ShopSettings | null>(null);
const fetchShopSettings = async (): Promise<ShopSettings> => {
const { data } = await api.get<ShopSettings>('/shop-settings');
settings.value = data;
return data;
};
const updateSimplexLink = async (payload: UpdateSimplexLinkPayload): Promise<ShopSettings> => {
const { data } = await api.patch<ShopSettings>('/shop-settings/simplex-link', payload);
settings.value = data;
return data;
};
const updateShippingNote = async (payload: UpdateShippingNotePayload): Promise<ShopSettings> => {
const { data } = await api.patch<ShopSettings>('/shop-settings/shipping-note', payload);
settings.value = data;
return data;
};
const updateNotifications = async (payload: UpdateNotificationsPayload): Promise<ShopSettings> => {
const { data } = await api.patch<ShopSettings>('/shop-settings/notifications', payload);
settings.value = data;
return data;
};
const connectSimplexNotifications = async (payload: ConnectSimplexNotificationsPayload): Promise<ShopSettings> => {
const { data } = await api.post<ShopSettings>('/shop-settings/simplex-connect', payload);
settings.value = data;
return data;
};
const uploadShopLogo = async (file: File): Promise<ShopSettings> => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post<ShopSettings>('/shop-settings/logo', formData);
settings.value = data;
return data;
};
const uploadShopFavicon = async (file: File): Promise<ShopSettings> => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post<ShopSettings>('/shop-settings/favicon', formData);
settings.value = data;
return data;
};
return {
settings,
fetchShopSettings,
updateSimplexLink,
updateShippingNote,
updateNotifications,
connectSimplexNotifications,
uploadShopLogo,
uploadShopFavicon
};
});