|
@@ -0,0 +1,575 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <DefaultHeaderPage>
|
|
|
|
|
+ <template #after>
|
|
|
|
|
+ <div class="flex items-center gap-sm q-mt-md">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ flat
|
|
|
|
|
+ icon="mdi-arrow-left"
|
|
|
|
|
+ :label="$t('common.actions.back')"
|
|
|
|
|
+ color="violet-normal"
|
|
|
|
|
+ padding="8px 12px"
|
|
|
|
|
+ @click="router.push({ name: 'LojaPage' })"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </DefaultHeaderPage>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="q-pa-md">
|
|
|
|
|
+ <div v-if="loadingItem" class="flex flex-center q-py-xl">
|
|
|
|
|
+ <q-spinner color="primary" size="48px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-form v-else ref="formRef" @submit="onSubmit">
|
|
|
|
|
+ <q-card flat bordered class="q-pa-md q-mb-md">
|
|
|
|
|
+ <div class="text-subtitle1 text-weight-bold text-violet-dark q-mb-md">
|
|
|
|
|
+ {{ isEdit ? $t('common.actions.edit') + ' ' + $t('loja.product') : $t('common.actions.new') + ' ' + $t('loja.product') }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.name"
|
|
|
|
|
+ v-model:error="validationErrors.name"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ :label="$t('common.terms.name')"
|
|
|
|
|
+ class="col-12 col-md-8"
|
|
|
|
|
+ @update:model-value="validationErrors.name = null"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultSelect
|
|
|
|
|
+ v-model="form.status"
|
|
|
|
|
+ v-model:error="validationErrors.status"
|
|
|
|
|
+ :options="statusOptions"
|
|
|
|
|
+ :label="$t('common.terms.status')"
|
|
|
|
|
+ emit-value
|
|
|
|
|
+ map-options
|
|
|
|
|
+ class="col-12 col-md-4 input-violet"
|
|
|
|
|
+ />
|
|
|
|
|
+ <CategorySelect
|
|
|
|
|
+ v-model="selectedCategory"
|
|
|
|
|
+ v-model:error="validationErrors.category_id"
|
|
|
|
|
+ type="store"
|
|
|
|
|
+ class="col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.description"
|
|
|
|
|
+ v-model:error="validationErrors.description"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :label="$t('common.terms.description')"
|
|
|
|
|
+ autogrow
|
|
|
|
|
+ class="col-12"
|
|
|
|
|
+ @update:model-value="validationErrors.description = null"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card flat bordered class="row no-wrap">
|
|
|
|
|
+ <div class="col-12 col-md-7 q-pa-md">
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.price"
|
|
|
|
|
+ v-model:error="validationErrors.price"
|
|
|
|
|
+ :label="$t('parceiro.price')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ class="col-12 col-md-4"
|
|
|
|
|
+ @update:model-value="validationErrors.price = null"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.associate_price"
|
|
|
|
|
+ v-model:error="validationErrors.associate_price"
|
|
|
|
|
+ :label="$t('parceiro.associate_price')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ class="col-12 col-md-4"
|
|
|
|
|
+ @update:model-value="validationErrors.associate_price = null"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.supplier_price"
|
|
|
|
|
+ v-model:error="validationErrors.supplier_price"
|
|
|
|
|
+ :label="$t('loja.supplier_price')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ class="col-12 col-md-4"
|
|
|
|
|
+ @update:model-value="validationErrors.supplier_price = null"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="q-mt-md">
|
|
|
|
|
+ <div class="text-subtitle2 text-violet-normal q-mb-sm">{{ $t('loja.variation') }}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-btn-toggle
|
|
|
|
|
+ v-model="variationType"
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ toggle-color="purple-8"
|
|
|
|
|
+ :options="variationTypeOptions"
|
|
|
|
|
+ class="q-mb-md"
|
|
|
|
|
+ @update:model-value="onVariationTypeChange"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="variationType === 'tamanho'" class="row q-col-gutter-sm">
|
|
|
|
|
+ <div v-for="size in sizeSlots" :key="size.label" class="col-12 col-sm-6 col-md-3">
|
|
|
|
|
+ <q-card flat bordered class="q-pa-sm">
|
|
|
|
|
+ <div class="flex items-center justify-between q-mb-xs">
|
|
|
|
|
+ <span class="text-weight-medium">{{ size.label }}</span>
|
|
|
|
|
+ <q-toggle v-model="size.enabled" color="purple-8" dense @update:model-value="syncSizeVariations" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template v-if="size.enabled">
|
|
|
|
|
+ <div class="row q-col-gutter-xs">
|
|
|
|
|
+ <div class="col-6">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="size.value"
|
|
|
|
|
+ :label="$t('loja.variation_value')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ dense
|
|
|
|
|
+ @update:model-value="syncSizeVariations"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-6">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="size.stock"
|
|
|
|
|
+ :label="$t('loja.variation_stock')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ dense
|
|
|
|
|
+ @update:model-value="syncSizeVariations"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else>
|
|
|
|
|
+ <div class="row q-gutter-sm q-mb-sm items-end">
|
|
|
|
|
+ <div class="col">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="newVariationLabel"
|
|
|
|
|
+ :label="$t('loja.variation_label')"
|
|
|
|
|
+ :placeholder="variationType === 'cor' ? $t('loja.variation_cor_placeholder') : $t('loja.variation_modelo_placeholder')"
|
|
|
|
|
+ dense
|
|
|
|
|
+ @keyup.enter="addVariation"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="newVariationStock"
|
|
|
|
|
+ :label="$t('loja.variation_stock')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ dense
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-auto">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="newVariationValue"
|
|
|
|
|
+ :label="$t('loja.variation_value')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ dense
|
|
|
|
|
+ style="width: 130px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-auto q-pb-xs">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ color="purple-8"
|
|
|
|
|
+ :label="$t('loja.add_variation')"
|
|
|
|
|
+ icon="mdi-plus"
|
|
|
|
|
+ padding="6px 12px"
|
|
|
|
|
+ :disable="!newVariationLabel.trim()"
|
|
|
|
|
+ :loading="addingVariation"
|
|
|
|
|
+ @click="addVariation"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="variations.length === 0" class="text-caption text-grey-5 q-mt-xs">
|
|
|
|
|
+ {{ $t('loja.variation_empty') }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-for="(v, idx) in variations" :key="v.id ?? idx" class="row q-gutter-xs q-mb-xs items-end">
|
|
|
|
|
+ <div class="col">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="v.variation_label"
|
|
|
|
|
+ :label="$t('loja.variation_label')"
|
|
|
|
|
+ dense
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="v.stock"
|
|
|
|
|
+ :label="$t('loja.variation_stock')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ dense
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-auto">
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="v.variation_value"
|
|
|
|
|
+ :label="$t('loja.variation_value')"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ prefix="R$"
|
|
|
|
|
+ dense
|
|
|
|
|
+ style="width: 130px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-auto q-pb-xs">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ flat
|
|
|
|
|
+ round
|
|
|
|
|
+ dense
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ color="negative"
|
|
|
|
|
+ icon="mdi-delete-outline"
|
|
|
|
|
+ :loading="v.deleting"
|
|
|
|
|
+ @click="removeVariation(idx, v)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex justify-end q-mt-lg">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ class="btn-gradient"
|
|
|
|
|
+ :label="$t('common.actions.save')"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ padding="10px 24px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-separator vertical />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="col-12 col-md-5 q-pa-md">
|
|
|
|
|
+ <div class="text-subtitle2 text-weight-bold text-violet-dark q-mb-md">{{ $t('loja.media') }}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <div class="col-6 col-sm-4">
|
|
|
|
|
+ <DefaultFilePicker
|
|
|
|
|
+ v-model="newMediaFile"
|
|
|
|
|
+ type="image"
|
|
|
|
|
+ :label="null"
|
|
|
|
|
+ @update:model-value="onNewMediaSelected"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="media in existingMedia"
|
|
|
|
|
+ :key="media.id"
|
|
|
|
|
+ class="col-6 col-sm-4"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="media-thumb">
|
|
|
|
|
+ <img :src="media.url" :alt="media.id" class="media-thumb-img" />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ round unelevated dense size="xs"
|
|
|
|
|
+ color="negative"
|
|
|
|
|
+ icon="mdi-close"
|
|
|
|
|
+ class="media-thumb-delete"
|
|
|
|
|
+ :loading="deletingMediaId === media.id"
|
|
|
|
|
+ @click="onDeleteMedia(media)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="uploadingMedia" class="text-caption text-grey-6 q-mt-sm">
|
|
|
|
|
+ <q-spinner size="16px" class="q-mr-xs" />{{ $t('loja.uploading_media') }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </q-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, watch, computed, onMounted } from "vue";
|
|
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
|
|
+import { useQuasar } from "quasar";
|
|
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
|
|
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
|
|
|
|
|
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
|
|
+import { useInputRules } from "src/composables/useInputRules";
|
|
|
|
|
+import {
|
|
|
|
|
+ getStoreItem,
|
|
|
|
|
+ createStoreItem,
|
|
|
|
|
+ updateStoreItem,
|
|
|
|
|
+ uploadStoreItemMedia,
|
|
|
|
|
+ deleteStoreItemMedia,
|
|
|
|
|
+ addStoreItemVariation,
|
|
|
|
|
+ deleteStoreItemVariation,
|
|
|
|
|
+} from "src/api/storeItem";
|
|
|
|
|
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
|
|
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
|
|
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
|
|
+import DefaultFilePicker from "src/components/defaults/DefaultFilePicker.vue";
|
|
|
|
|
+import CategorySelect from "src/components/selects/CategorySelect.vue";
|
|
|
|
|
+
|
|
|
|
|
+const route = useRoute();
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const $q = useQuasar();
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+const formRef = ref(null);
|
|
|
|
|
+const { inputRules } = useInputRules();
|
|
|
|
|
+
|
|
|
|
|
+const isEdit = computed(() => !!route.params.id);
|
|
|
|
|
+const loadingItem = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+const { form, getUpdatedFields } = useFormUpdateTracker({
|
|
|
|
|
+ name: "",
|
|
|
|
|
+ description: "",
|
|
|
|
|
+ category_id: null,
|
|
|
|
|
+ price: null,
|
|
|
|
|
+ associate_price: null,
|
|
|
|
|
+ supplier_price: null,
|
|
|
|
|
+ status: "active",
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const { loading, validationErrors, execute: submitForm } = useSubmitHandler({
|
|
|
|
|
+ onSuccess: () => router.push({ name: "LojaPage" }),
|
|
|
|
|
+ formRef,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const statusOptions = [
|
|
|
|
|
+ { label: t("common.status.active"), value: "active" },
|
|
|
|
|
+ { label: t("common.status.inactive"), value: "inactive" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const selectedCategory = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const variationType = ref("tamanho");
|
|
|
|
|
+const variations = ref([]);
|
|
|
|
|
+const newVariationLabel = ref("");
|
|
|
|
|
+const newVariationValue = ref(null);
|
|
|
|
|
+const newVariationStock = ref(null);
|
|
|
|
|
+const addingVariation = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+const variationTypeOptions = computed(() => [
|
|
|
|
|
+ { label: t("loja.variation_tamanho"), value: "tamanho" },
|
|
|
|
|
+ { label: t("loja.variation_cor"), value: "cor" },
|
|
|
|
|
+ { label: t("loja.variation_modelo"), value: "modelo" },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const sizeSlots = reactive([
|
|
|
|
|
+ { label: "P", enabled: false, value: null, stock: null },
|
|
|
|
|
+ { label: "M", enabled: false, value: null, stock: null },
|
|
|
|
|
+ { label: "G", enabled: false, value: null, stock: null },
|
|
|
|
|
+ { label: "GG", enabled: false, value: null, stock: null },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const syncSizeVariations = () => {
|
|
|
|
|
+ variations.value = sizeSlots
|
|
|
|
|
+ .filter((s) => s.enabled)
|
|
|
|
|
+ .map((s) => ({
|
|
|
|
|
+ variation_type: "tamanho",
|
|
|
|
|
+ variation_label: s.label,
|
|
|
|
|
+ variation_value: s.value ?? null,
|
|
|
|
|
+ stock: s.stock ? Number(s.stock) : 0,
|
|
|
|
|
+ }));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onVariationTypeChange = () => {
|
|
|
|
|
+ variations.value = [];
|
|
|
|
|
+ newVariationLabel.value = "";
|
|
|
|
|
+ newVariationValue.value = null;
|
|
|
|
|
+ newVariationStock.value = null;
|
|
|
|
|
+ sizeSlots.forEach((s) => { s.enabled = false; s.value = null; s.stock = null; });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const addVariation = async () => {
|
|
|
|
|
+ const label = newVariationLabel.value.trim();
|
|
|
|
|
+ if (!label) return;
|
|
|
|
|
+
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ variation_type: variationType.value,
|
|
|
|
|
+ variation_label: label,
|
|
|
|
|
+ variation_value: newVariationValue.value ?? null,
|
|
|
|
|
+ stock: newVariationStock.value ? Number(newVariationStock.value) : 0,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (isEdit.value) {
|
|
|
|
|
+ addingVariation.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const created = await addStoreItemVariation(route.params.id, payload);
|
|
|
|
|
+ variations.value.push(created);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ addingVariation.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ variations.value.push(payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ newVariationLabel.value = "";
|
|
|
|
|
+ newVariationValue.value = null;
|
|
|
|
|
+ newVariationStock.value = null;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const removeVariation = async (idx, v) => {
|
|
|
|
|
+ if (isEdit.value && v?.id) {
|
|
|
|
|
+ v.deleting = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await deleteStoreItemVariation(route.params.id, v.id);
|
|
|
|
|
+ variations.value.splice(idx, 1);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ v.deleting = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ variations.value.splice(idx, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+const existingMedia = ref([]);
|
|
|
|
|
+const newMediaFile = ref(null);
|
|
|
|
|
+const uploadingMedia = ref(false);
|
|
|
|
|
+const deletingMediaId = ref(null);
|
|
|
|
|
+const pendingFiles = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+const onNewMediaSelected = async (file) => {
|
|
|
|
|
+ if (!(file instanceof File)) return;
|
|
|
|
|
+ if (isEdit.value) {
|
|
|
|
|
+ uploadingMedia.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const media = await uploadStoreItemMedia(route.params.id, file);
|
|
|
|
|
+ existingMedia.value.push(media);
|
|
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ uploadingMedia.value = false;
|
|
|
|
|
+ newMediaFile.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ pendingFiles.value.push(file);
|
|
|
|
|
+ newMediaFile.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onDeleteMedia = async (media) => {
|
|
|
|
|
+ deletingMediaId.value = media.id;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await deleteStoreItemMedia(route.params.id, media.id);
|
|
|
|
|
+ existingMedia.value = existingMedia.value.filter((m) => m.id !== media.id);
|
|
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ deletingMediaId.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onSubmit = async () => {
|
|
|
|
|
+ if (isEdit.value) {
|
|
|
|
|
+ await submitForm(() =>
|
|
|
|
|
+ updateStoreItem(route.params.id, {
|
|
|
|
|
+ ...getUpdatedFields.value,
|
|
|
|
|
+ variations: variations.value,
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await submitForm(async () => {
|
|
|
|
|
+ const created = await createStoreItem({ ...form, variations: variations.value });
|
|
|
|
|
+ for (const file of pendingFiles.value) {
|
|
|
|
|
+ await uploadStoreItemMedia(created.id, file);
|
|
|
|
|
+ }
|
|
|
|
|
+ return created;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+watch(selectedCategory, (val) => { form.category_id = val?.value ?? null; });
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ if (!isEdit.value) return;
|
|
|
|
|
+ loadingItem.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const item = await getStoreItem(route.params.id);
|
|
|
|
|
+
|
|
|
|
|
+ form.name = item.name ?? "";
|
|
|
|
|
+ form.description = item.description ?? "";
|
|
|
|
|
+ form.category_id = item.category_id ?? null;
|
|
|
|
|
+ form.price = item.price ?? null;
|
|
|
|
|
+ form.associate_price = item.associate_price ?? null;
|
|
|
|
|
+ form.supplier_price = item.supplier_price ?? null;
|
|
|
|
|
+ form.status = item.status?.value ?? item.status ?? "active";
|
|
|
|
|
+
|
|
|
|
|
+ existingMedia.value = item.media ?? [];
|
|
|
|
|
+
|
|
|
|
|
+ if (item.category) {
|
|
|
|
|
+ selectedCategory.value = {
|
|
|
|
|
+ label: item.category.name,
|
|
|
|
|
+ value: item.category_id,
|
|
|
|
|
+ type: item.category.type,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (item.variations?.length) {
|
|
|
|
|
+ const type = item.variations[0].variation_type;
|
|
|
|
|
+ variationType.value = type;
|
|
|
|
|
+
|
|
|
|
|
+ if (type === "tamanho") {
|
|
|
|
|
+ const labelMap = { P: 0, M: 1, G: 2, GG: 3 };
|
|
|
|
|
+ item.variations.forEach((v) => {
|
|
|
|
|
+ const idx = labelMap[v.variation_label];
|
|
|
|
|
+ if (idx !== undefined) {
|
|
|
|
|
+ sizeSlots[idx].enabled = true;
|
|
|
|
|
+ sizeSlots[idx].value = v.variation_value;
|
|
|
|
|
+ sizeSlots[idx].stock = v.stock ?? null;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ syncSizeVariations();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ variations.value = item.variations.map((v) => ({
|
|
|
|
|
+ variation_type: v.variation_type,
|
|
|
|
|
+ variation_label: v.variation_label,
|
|
|
|
|
+ variation_value: v.variation_value,
|
|
|
|
|
+ stock: v.stock ?? 0,
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingItem.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.btn-gradient {
|
|
|
|
|
+ background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
|
|
|
|
|
+ color: white !important;
|
|
|
|
|
+ border-radius: 8px !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-violet-dark { color: #4d1658; }
|
|
|
|
|
+
|
|
|
|
|
+.gap-sm { gap: 8px; }
|
|
|
|
|
+
|
|
|
|
|
+.media-thumb {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ height: 200px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 1px solid rgba(102, 29, 117, 0.2);
|
|
|
|
|
+ background: #f5f0f7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-thumb-img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.media-thumb-delete {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 4px;
|
|
|
|
|
+ right: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|