Replace generic "Required" copy with field-specific messages for product, variant, and digital stock forms.
764 lines
26 KiB
Vue
764 lines
26 KiB
Vue
<template>
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<span>Digital stock</span>
|
|
</template>
|
|
|
|
<el-text tag="p" size="small" class="secondary-text w-full m-0 mb-16">
|
|
Content along with all attachments is delivered automatically after purchase. Content and attachments are
|
|
encrypted at rest on the server.
|
|
</el-text>
|
|
|
|
<p class="mb-16">Currently available: {{ currentVariant?.stockAvailable }}</p>
|
|
|
|
<el-form
|
|
ref="stockAddFormRef"
|
|
label-position="top"
|
|
:model="stockAddForm"
|
|
:rules="stockContentRules"
|
|
class="mb-24"
|
|
>
|
|
<el-form-item label="Add content" prop="content">
|
|
<div class="flex gap-12 w-full">
|
|
<el-input
|
|
v-model="stockAddForm.content"
|
|
type="textarea"
|
|
:rows="3"
|
|
class="flex-1"
|
|
:disabled="stockRemoving"
|
|
@input="stockAddFormRef?.clearValidate('content')"
|
|
/>
|
|
<el-button type="primary" :loading="stockAdding" :disabled="stockRemoving" @click="submitAddStock">
|
|
Add
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="Attachments (optional)">
|
|
<div class="stock-attachments-field flex flex-col gap-12 w-full">
|
|
<el-text tag="p" size="small" class="secondary-text w-full">
|
|
{{ stockAttachmentsHint }}
|
|
</el-text>
|
|
|
|
<div class="stock-attachment-upload-row flex items-center gap-12 min-w-0">
|
|
<el-upload
|
|
ref="stockAddUploadRef"
|
|
:show-file-list="false"
|
|
:auto-upload="false"
|
|
multiple
|
|
:accept="digitalStockAttachmentAccept"
|
|
:disabled="
|
|
stockAdding ||
|
|
stockRemoving ||
|
|
stockAddPendingFiles.length >= validationDigitalStockAttachmentsMax
|
|
"
|
|
:on-change="onStockAddFileChange"
|
|
>
|
|
<el-button
|
|
type="default"
|
|
:disabled="stockAddPendingFiles.length >= validationDigitalStockAttachmentsMax"
|
|
>
|
|
Select files
|
|
</el-button>
|
|
</el-upload>
|
|
</div>
|
|
|
|
<ul v-if="stockAddPendingFiles.length" class="stock-pending-files m-0 p-0">
|
|
<li
|
|
v-for="(file, index) in stockAddPendingFiles"
|
|
:key="`${file.name}-${file.size}-${index}`"
|
|
class="stock-pending-files__item flex items-center justify-between gap-12 py-4 min-w-0"
|
|
>
|
|
<el-text size="small" class="flex-ellipsis" :title="file.name">
|
|
{{ file.name }}
|
|
</el-text>
|
|
<el-button
|
|
link
|
|
type="danger"
|
|
class="flex-shrink-0"
|
|
:disabled="stockAdding || stockRemoving"
|
|
@click="removeStockAddPendingFile(index)"
|
|
>
|
|
Remove
|
|
</el-button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<div class="flex items-center gap-12 mb-16">
|
|
<el-switch v-model="hideSold" active-text="Hide sold" @change="() => loadStockList({ page: 1 })" />
|
|
</div>
|
|
|
|
<div class="cms-table-scroll">
|
|
<el-table
|
|
v-loading="stockListLoading || stockRemoving"
|
|
:element-loading-text="stockRemoving ? 'Removing stock…' : 'Loading stock…'"
|
|
:data="items"
|
|
stripe
|
|
empty-text="No stock lines yet"
|
|
>
|
|
<el-table-column prop="content" label="Content" min-width="200" show-overflow-tooltip />
|
|
|
|
<el-table-column label="Attachments" width="120">
|
|
<template #default="{ row: item }">
|
|
<el-tooltip
|
|
v-if="item.attachments?.length"
|
|
:content="formatAttachmentNames(item.attachments)"
|
|
placement="top"
|
|
>
|
|
<span
|
|
>{{ item.attachments.length }} file{{ item.attachments.length === 1 ? '' : 's' }}</span
|
|
>
|
|
</el-tooltip>
|
|
<span v-else>—</span>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column prop="isSold" label="Sold" width="80" />
|
|
|
|
<el-table-column label="Updated" width="180">
|
|
<template #default="{ row: item }">
|
|
{{ formatDate(item.updatedAt) }}
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column width="180" fixed="right">
|
|
<template #default="{ row: item }">
|
|
<el-button
|
|
link
|
|
type="primary"
|
|
:disabled="item.isSold || stockRemoving"
|
|
@click="openEditStock(item)"
|
|
>
|
|
Edit
|
|
</el-button>
|
|
|
|
<el-button
|
|
link
|
|
type="danger"
|
|
:disabled="item.isSold || stockRemoving"
|
|
@click="confirmRemoveStock(item)"
|
|
>
|
|
Delete
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
|
|
<cms-list-pagination v-model:page="page" v-model:limit="limit" :total="total" @change="() => loadStockList()" />
|
|
</el-card>
|
|
|
|
<el-dialog
|
|
v-model="stockEditVisible"
|
|
title="Edit stock item"
|
|
width="520px"
|
|
destroy-on-close
|
|
@closed="stockEditItem = null"
|
|
>
|
|
<el-form ref="stockEditFormRef" label-position="top" :model="stockEditForm" :rules="stockContentRules">
|
|
<el-form-item label="Content" prop="content">
|
|
<el-input
|
|
v-model="stockEditForm.content"
|
|
type="textarea"
|
|
:rows="6"
|
|
@input="stockEditFormRef?.clearValidate('content')"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="Attachments">
|
|
<div class="stock-attachments-field flex flex-col gap-12 w-full">
|
|
<el-text tag="p" size="small" class="secondary-text w-full">
|
|
{{ stockAttachmentsHint }}
|
|
</el-text>
|
|
|
|
<ul v-if="stockEditAttachments.length" class="stock-edit-attachments m-0 p-0 secondary-text">
|
|
<li
|
|
v-for="attachment in stockEditAttachments"
|
|
:key="attachment.id"
|
|
class="stock-edit-attachments__item flex items-center justify-between gap-12 py-4 min-w-0"
|
|
>
|
|
<div class="stock-edit-attachments__meta flex items-start gap-8 min-h-0 flex-1 min-w-0">
|
|
<span
|
|
class="flex-ellipsis"
|
|
:title="attachment.originalFilename"
|
|
>
|
|
{{ attachment.originalFilename }}
|
|
</span>
|
|
<span class="secondary-text flex-shrink-0">
|
|
{{ formatFileSize(attachment.sizeBytes) }}
|
|
</span>
|
|
</div>
|
|
<div class="stock-edit-attachments__actions flex gap-4">
|
|
<el-button
|
|
link
|
|
type="primary"
|
|
:loading="stockEditDownloadingId === attachment.id"
|
|
:disabled="stockEditAttachmentBusy"
|
|
@click="downloadStockAttachment(attachment)"
|
|
>
|
|
Download
|
|
</el-button>
|
|
<el-button
|
|
link
|
|
type="danger"
|
|
:loading="stockEditRemovingAttachmentId === attachment.id"
|
|
:disabled="stockEditItem?.isSold || stockEditAttachmentBusy"
|
|
@click="confirmRemoveStockAttachment(attachment.id)"
|
|
>
|
|
Remove
|
|
</el-button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
|
|
<el-text v-else tag="p" size="small" class="secondary-text w-full m-0">No attachments yet.</el-text>
|
|
|
|
<div class="stock-attachment-upload-row flex items-center gap-12 min-w-0">
|
|
<el-upload
|
|
ref="stockEditUploadRef"
|
|
:show-file-list="false"
|
|
:auto-upload="false"
|
|
:limit="1"
|
|
:accept="digitalStockAttachmentAccept"
|
|
:disabled="
|
|
stockEditItem?.isSold ||
|
|
stockEditAttachmentBusy ||
|
|
stockEditAttachments.length >= validationDigitalStockAttachmentsMax
|
|
"
|
|
:on-change="onStockEditFileChange"
|
|
:on-remove="onStockEditFileRemove"
|
|
>
|
|
<el-button
|
|
type="default"
|
|
:disabled="
|
|
stockEditItem?.isSold ||
|
|
stockEditAttachments.length >= validationDigitalStockAttachmentsMax
|
|
"
|
|
>
|
|
Select file
|
|
</el-button>
|
|
</el-upload>
|
|
|
|
<template v-if="stockEditPendingFile">
|
|
<el-text
|
|
size="small"
|
|
class="flex-ellipsis"
|
|
:title="stockEditPendingFile.name"
|
|
>
|
|
{{ stockEditPendingFile.name }}
|
|
</el-text>
|
|
<el-button
|
|
type="primary"
|
|
:loading="stockEditUploading"
|
|
:disabled="stockEditItem?.isSold || stockEditAttachmentBusy"
|
|
@click="submitStockEditUpload"
|
|
>
|
|
Upload
|
|
</el-button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="stockEditVisible = false">Cancel</el-button>
|
|
<el-button type="primary" :loading="stockEditSaving" @click="submitEditStock">Save</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { config } from '@/config';
|
|
import { useDigitalStockStore } from '@/stores/digitalStock';
|
|
import { useProductsStore } from '@/stores/products';
|
|
import { getPaginationLastPage } from '@/utils/getPaginationLastPage';
|
|
import type { DigitalStockAttachment } from '@/types/product/DigitalStockAttachment';
|
|
import type { DigitalStockItem } from '@/types/product/DigitalStockItem';
|
|
import type { DigitalStockListQuery } from '@/types/product/DigitalStockListQuery';
|
|
import { buildUploadHint } from '@/utils/upload/buildUploadHint';
|
|
import { formatDate } from '@/utils/formatDate';
|
|
import { formatFileSize } from '@/utils/formatFileSize';
|
|
import { validateUpload } from '@/utils/upload/validateUpload';
|
|
import {
|
|
ElMessage,
|
|
ElMessageBox,
|
|
type FormInstance,
|
|
type FormRules,
|
|
type UploadInstance,
|
|
type UploadProps
|
|
} from 'element-plus';
|
|
import { storeToRefs } from 'pinia';
|
|
import { computed, nextTick, onBeforeMount, reactive, ref } from 'vue';
|
|
|
|
const {
|
|
digitalStockAttachment: { accept: digitalStockAttachmentAccept, maxFileBytes: digitalStockAttachmentMaxFileBytes },
|
|
validation: { digitalStockAttachmentsMax: validationDigitalStockAttachmentsMax }
|
|
} = config;
|
|
|
|
const productsStore = useProductsStore();
|
|
const digitalStockStore = useDigitalStockStore();
|
|
|
|
const { currentVariant, currentProductId, currentVariantId } = storeToRefs(productsStore);
|
|
const { items } = storeToRefs(digitalStockStore);
|
|
|
|
const page = ref(1);
|
|
const limit = ref(20);
|
|
const total = ref(0);
|
|
const hideSold = ref(true);
|
|
|
|
const stockAddFormRef = ref<FormInstance>();
|
|
const stockAddForm = reactive({ content: '' });
|
|
const stockAdding = ref(false);
|
|
const stockRemoving = ref(false);
|
|
const stockListLoading = ref(false);
|
|
|
|
const stockContentRules: FormRules = {
|
|
content: [{ required: true, message: 'Content is required', trigger: 'blur' }]
|
|
};
|
|
|
|
const stockEditVisible = ref(false);
|
|
const stockEditItem = ref<DigitalStockItem | null>(null);
|
|
const stockEditFormRef = ref<FormInstance>();
|
|
const stockEditForm = reactive({ content: '' });
|
|
const stockEditSaving = ref(false);
|
|
|
|
const stockAddUploadRef = ref<UploadInstance>();
|
|
const stockAddPendingFiles = ref<File[]>([]);
|
|
|
|
const stockEditUploadRef = ref<UploadInstance>();
|
|
const stockEditPendingFile = ref<File | null>(null);
|
|
const stockEditUploading = ref(false);
|
|
const stockEditDownloadingId = ref<string | null>(null);
|
|
const stockEditRemovingAttachmentId = ref<string | null>(null);
|
|
|
|
let suppressPaginationChange = false;
|
|
|
|
onBeforeMount(() => {
|
|
digitalStockStore.resetList();
|
|
|
|
loadStockList();
|
|
});
|
|
|
|
const stockAttachmentsHint = computed(() =>
|
|
buildUploadHint({
|
|
allowedMimesCsv: digitalStockAttachmentAccept,
|
|
maxFileBytes: digitalStockAttachmentMaxFileBytes,
|
|
maxFiles: validationDigitalStockAttachmentsMax,
|
|
maxFilesLabel: 'attachments per stock item',
|
|
encryptedAtRest: true
|
|
})
|
|
);
|
|
|
|
const stockEditAttachments = computed(() => {
|
|
const item = stockEditItem.value;
|
|
|
|
if (!item) {
|
|
return [];
|
|
}
|
|
|
|
const current = items.value.find(stockItem => stockItem.id === item.id);
|
|
|
|
return current?.attachments ?? item.attachments ?? [];
|
|
});
|
|
|
|
const stockEditAttachmentBusy = computed(
|
|
() =>
|
|
stockEditUploading.value ||
|
|
stockEditDownloadingId.value !== null ||
|
|
stockEditRemovingAttachmentId.value !== null
|
|
);
|
|
|
|
/**
|
|
* Fetches the paginated digital stock list for the current variant.
|
|
*
|
|
* Query params are built from component refs (`page`, `limit`, `hideSold`), with optional
|
|
* `overrides` for programmatic jumps (e.g. reset to page 1 on filter change, go to last
|
|
* page after add). Server response reconciles `total`, `page`, and `limit`.
|
|
*
|
|
* `el-pagination` emits `@change` after programmatic v-model updates (flush: post), not only
|
|
* on user clicks. Reconcile after fetch would otherwise trigger a duplicate fetch via
|
|
* `@change="loadStockList()"`. We set `suppressPaginationChange` while assigning refs and
|
|
* clear it after `nextTick()` so the echo call is ignored at the top of this function.
|
|
*
|
|
* @param overrides - Partial query merged with current refs; omitted keys fall back to refs.
|
|
*/
|
|
const loadStockList = async (overrides: Partial<DigitalStockListQuery> = {}): Promise<void> => {
|
|
if (suppressPaginationChange) {
|
|
return;
|
|
}
|
|
|
|
if (!currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
const query: DigitalStockListQuery = {
|
|
page: overrides.page ?? page.value,
|
|
limit: overrides.limit ?? limit.value,
|
|
hideSold: overrides.hideSold ?? hideSold.value
|
|
};
|
|
|
|
stockListLoading.value = true;
|
|
|
|
try {
|
|
const data = await digitalStockStore.fetchList(currentProductId.value, currentVariantId.value, query);
|
|
|
|
suppressPaginationChange = true;
|
|
total.value = data.total;
|
|
page.value = data.page;
|
|
limit.value = data.limit;
|
|
await nextTick();
|
|
suppressPaginationChange = false;
|
|
} finally {
|
|
stockListLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const formatAttachmentNames = (attachments: DigitalStockAttachment[]): string =>
|
|
attachments.map(attachment => attachment.originalFilename).join(', ');
|
|
|
|
const clearStockAddForm = (): void => {
|
|
stockAddForm.content = '';
|
|
stockAddPendingFiles.value = [];
|
|
stockAddUploadRef.value?.clearFiles();
|
|
stockAddFormRef.value?.clearValidate();
|
|
};
|
|
|
|
const submitAddStock = async (): Promise<void> => {
|
|
const form = stockAddFormRef.value;
|
|
|
|
if (!form || !currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await form.validate();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
for (const file of stockAddPendingFiles.value) {
|
|
const err = validateUpload(file, {
|
|
allowedMimesCsv: digitalStockAttachmentAccept,
|
|
maxFileBytes: digitalStockAttachmentMaxFileBytes
|
|
});
|
|
|
|
if (err) {
|
|
ElMessage.error(`${file.name}: ${err}`);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (stockAddPendingFiles.value.length > validationDigitalStockAttachmentsMax) {
|
|
ElMessage.error(`A stock item can have at most ${validationDigitalStockAttachmentsMax} attachments`);
|
|
|
|
return;
|
|
}
|
|
|
|
stockAdding.value = true;
|
|
|
|
const pendingFiles = [...stockAddPendingFiles.value];
|
|
|
|
try {
|
|
const newItem = await digitalStockStore.addItem(
|
|
currentProductId.value,
|
|
currentVariantId.value,
|
|
stockAddForm.content.trim()
|
|
);
|
|
|
|
const newLastPage = getPaginationLastPage(total.value + 1, limit.value);
|
|
|
|
await loadStockList({ page: newLastPage });
|
|
|
|
clearStockAddForm();
|
|
|
|
if (pendingFiles.length === 0) {
|
|
ElMessage.success('Stock item added');
|
|
|
|
return;
|
|
}
|
|
|
|
const results = await Promise.allSettled(
|
|
pendingFiles.map(file =>
|
|
digitalStockStore.uploadAttachment(currentProductId.value!, currentVariantId.value!, newItem.id, file)
|
|
)
|
|
);
|
|
|
|
const failedCount = results.filter(result => result.status === 'rejected').length;
|
|
|
|
if (failedCount === 0) {
|
|
ElMessage.success('Stock item added and attachments uploaded');
|
|
} else if (failedCount === pendingFiles.length) {
|
|
ElMessage.warning('Stock item added, but attachments could not be uploaded');
|
|
} else {
|
|
ElMessage.warning(
|
|
`Stock item added, but ${failedCount} of ${pendingFiles.length} attachments could not be uploaded`
|
|
);
|
|
}
|
|
} catch {
|
|
ElMessage.error('Could not add stock');
|
|
} finally {
|
|
stockAdding.value = false;
|
|
}
|
|
};
|
|
|
|
const onStockAddFileChange: UploadProps['onChange'] = uploadFile => {
|
|
const raw = uploadFile.raw;
|
|
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
|
|
if (stockAddPendingFiles.value.length >= validationDigitalStockAttachmentsMax) {
|
|
ElMessage.error(`A stock item can have at most ${validationDigitalStockAttachmentsMax} attachments`);
|
|
stockAddUploadRef.value?.clearFiles();
|
|
|
|
return;
|
|
}
|
|
|
|
const err = validateUpload(raw, {
|
|
allowedMimesCsv: digitalStockAttachmentAccept,
|
|
maxFileBytes: digitalStockAttachmentMaxFileBytes
|
|
});
|
|
|
|
if (err) {
|
|
ElMessage.error(err);
|
|
stockAddUploadRef.value?.clearFiles();
|
|
|
|
return;
|
|
}
|
|
|
|
stockAddPendingFiles.value = [...stockAddPendingFiles.value, raw];
|
|
stockAddUploadRef.value?.clearFiles();
|
|
};
|
|
|
|
const removeStockAddPendingFile = (index: number): void => {
|
|
stockAddPendingFiles.value = stockAddPendingFiles.value.filter((_, i) => i !== index);
|
|
};
|
|
|
|
const openEditStock = (item: DigitalStockItem): void => {
|
|
stockEditItem.value = item;
|
|
stockEditForm.content = item.content;
|
|
stockEditPendingFile.value = null;
|
|
stockEditUploadRef.value?.clearFiles();
|
|
stockEditVisible.value = true;
|
|
};
|
|
|
|
const onStockEditFileChange: UploadProps['onChange'] = uploadFile => {
|
|
const raw = uploadFile.raw;
|
|
|
|
if (raw) {
|
|
const err = validateUpload(raw, {
|
|
allowedMimesCsv: digitalStockAttachmentAccept,
|
|
maxFileBytes: digitalStockAttachmentMaxFileBytes
|
|
});
|
|
|
|
if (err) {
|
|
ElMessage.error(err);
|
|
stockEditUploadRef.value?.clearFiles();
|
|
stockEditPendingFile.value = null;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
stockEditPendingFile.value = raw ?? null;
|
|
};
|
|
|
|
const onStockEditFileRemove: UploadProps['onRemove'] = () => {
|
|
stockEditPendingFile.value = null;
|
|
};
|
|
|
|
const submitStockEditUpload = async (): Promise<void> => {
|
|
const item = stockEditItem.value;
|
|
const file = stockEditPendingFile.value;
|
|
|
|
if (!file || !item || !currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
const err = validateUpload(file, {
|
|
allowedMimesCsv: digitalStockAttachmentAccept,
|
|
maxFileBytes: digitalStockAttachmentMaxFileBytes
|
|
});
|
|
|
|
if (err) {
|
|
ElMessage.error(err);
|
|
|
|
return;
|
|
}
|
|
|
|
stockEditUploading.value = true;
|
|
|
|
try {
|
|
await digitalStockStore.uploadAttachment(currentProductId.value, currentVariantId.value, item.id, file);
|
|
|
|
stockEditPendingFile.value = null;
|
|
stockEditUploadRef.value?.clearFiles();
|
|
ElMessage.success('Attachment uploaded');
|
|
} catch {
|
|
ElMessage.error('Could not upload attachment');
|
|
} finally {
|
|
stockEditUploading.value = false;
|
|
}
|
|
};
|
|
|
|
const downloadStockAttachment = async (attachment: DigitalStockAttachment): Promise<void> => {
|
|
const item = stockEditItem.value;
|
|
|
|
if (!item || !currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
stockEditDownloadingId.value = attachment.id;
|
|
|
|
try {
|
|
await digitalStockStore.downloadAttachment(
|
|
currentProductId.value,
|
|
currentVariantId.value,
|
|
item.id,
|
|
attachment.id,
|
|
attachment.originalFilename
|
|
);
|
|
} catch {
|
|
ElMessage.error('Could not download attachment');
|
|
} finally {
|
|
stockEditDownloadingId.value = null;
|
|
}
|
|
};
|
|
|
|
const confirmRemoveStockAttachment = async (attachmentId: string): Promise<void> => {
|
|
const item = stockEditItem.value;
|
|
|
|
if (!item || !currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await ElMessageBox.confirm('Remove this attachment?', 'Delete attachment', {
|
|
type: 'warning',
|
|
confirmButtonText: 'Delete',
|
|
cancelButtonText: 'Cancel'
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
stockEditRemovingAttachmentId.value = attachmentId;
|
|
|
|
try {
|
|
await digitalStockStore.removeAttachment(currentProductId.value, currentVariantId.value, item.id, attachmentId);
|
|
|
|
ElMessage.success('Attachment removed');
|
|
} catch {
|
|
ElMessage.error('Could not remove attachment');
|
|
} finally {
|
|
stockEditRemovingAttachmentId.value = null;
|
|
}
|
|
};
|
|
|
|
const submitEditStock = async (): Promise<void> => {
|
|
const form = stockEditFormRef.value;
|
|
const item = stockEditItem.value;
|
|
|
|
if (!form || !item || !currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await form.validate();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
stockEditSaving.value = true;
|
|
|
|
try {
|
|
await digitalStockStore.updateItem(
|
|
currentProductId.value,
|
|
currentVariantId.value,
|
|
item.id,
|
|
stockEditForm.content.trim()
|
|
);
|
|
stockEditVisible.value = false;
|
|
ElMessage.success('Stock item updated');
|
|
} catch {
|
|
ElMessage.error('Could not update stock');
|
|
} finally {
|
|
stockEditSaving.value = false;
|
|
}
|
|
};
|
|
|
|
const confirmRemoveStock = async (item: DigitalStockItem): Promise<void> => {
|
|
if (!currentProductId.value || !currentVariantId.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await ElMessageBox.confirm('Remove this stock line?', 'Delete stock item', {
|
|
type: 'warning',
|
|
confirmButtonText: 'Delete',
|
|
cancelButtonText: 'Cancel'
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
stockRemoving.value = true;
|
|
|
|
try {
|
|
await digitalStockStore.removeItem(currentProductId.value, currentVariantId.value, item.id, item.isSold);
|
|
|
|
const nextTotal = Math.max(0, total.value - 1);
|
|
const nextPage = Math.min(page.value, getPaginationLastPage(nextTotal, limit.value));
|
|
|
|
await loadStockList({ page: nextPage });
|
|
|
|
ElMessage.success('Stock item removed');
|
|
} catch {
|
|
ElMessage.error('Could not remove stock');
|
|
} finally {
|
|
stockRemoving.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.stock-attachments-field {
|
|
line-height: 1.45;
|
|
|
|
:deep(.el-text) {
|
|
line-height: inherit;
|
|
}
|
|
}
|
|
|
|
.stock-attachment-upload-row {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stock-pending-files,
|
|
.stock-edit-attachments {
|
|
list-style: none;
|
|
}
|
|
|
|
.stock-edit-attachments__item {
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
|
|
.stock-edit-attachments__actions {
|
|
flex-shrink: 0;
|
|
|
|
:deep(.el-button + .el-button) {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|