changes 1
This commit is contained in:
+142
-149
@@ -14,7 +14,6 @@ import { useToast } from '@/composables/useToast'
|
||||
import { confirm } from '@/composables/useConfirm'
|
||||
import { formatCurrency, formatNumber } from '@/utils/format'
|
||||
import type { Company, Product } from '@/types'
|
||||
import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue'
|
||||
import { toCssBackgroundUrl } from '@/utils/catalogImage'
|
||||
import {
|
||||
PAYMENT_RECIPIENT_COMPANY_SLUG,
|
||||
@@ -64,18 +63,15 @@ const companyFormOptions = computed(() => {
|
||||
const emptyForm = () => ({
|
||||
companyId: '',
|
||||
name: '',
|
||||
color: '',
|
||||
productCode: '',
|
||||
minMeterage: 100,
|
||||
orderStep: 50,
|
||||
price: null as number | null,
|
||||
internalStock: 0,
|
||||
widthCm: null as number | null,
|
||||
weightGrams: null as number | null,
|
||||
inStock: true,
|
||||
isActive: true,
|
||||
usageCategory: 'other',
|
||||
season: 'all_season',
|
||||
composition: '',
|
||||
weaveType: '',
|
||||
subtitle: '',
|
||||
specs: '',
|
||||
rollMeters: 50,
|
||||
isNew: false,
|
||||
@@ -91,7 +87,7 @@ const form = reactive(emptyForm())
|
||||
|
||||
const isModalOpen = computed(() => modalMode.value !== null)
|
||||
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش محصول' : 'افزودن محصول'))
|
||||
const isFormValid = computed(() => form.name.trim() && form.minMeterage > 0)
|
||||
const isFormValid = computed(() => form.name.trim().length > 0 && form.rollMeters > 0)
|
||||
const hasActiveFilters = computed(
|
||||
() => searchQuery.value.trim().length > 0 || companyFilter.value !== 'all',
|
||||
)
|
||||
@@ -167,18 +163,15 @@ async function openEditModal(product: Product) {
|
||||
}
|
||||
form.companyId = productCompanyIdForForm(product.companyId, companies.value)
|
||||
form.name = product.name
|
||||
form.color = product.color
|
||||
form.productCode = product.productCode
|
||||
form.minMeterage = product.minMeterage
|
||||
form.orderStep = product.orderStep
|
||||
form.price = product.price
|
||||
form.internalStock = product.internalStock
|
||||
form.widthCm = product.widthCm ?? null
|
||||
form.weightGrams = product.weightGrams ?? null
|
||||
form.inStock = product.internalStock == null || product.internalStock > 0
|
||||
form.isActive = product.isActive
|
||||
form.usageCategory = (product as { usageCategory?: string }).usageCategory ?? 'other'
|
||||
form.season = (product as { season?: string }).season ?? 'all_season'
|
||||
form.composition = (product as { composition?: string }).composition ?? ''
|
||||
form.weaveType = (product as { weaveType?: string }).weaveType ?? ''
|
||||
form.subtitle = (product as { subtitle?: string }).subtitle ?? ''
|
||||
form.specs = (product as { specs?: string }).specs ?? ''
|
||||
form.rollMeters = (product as { rollMeters?: number }).rollMeters ?? 50
|
||||
form.isNew = (product as { isNew?: boolean }).isNew ?? false
|
||||
@@ -246,10 +239,7 @@ function buildProductMedia() {
|
||||
|
||||
form.colorRows.forEach((row, index) => {
|
||||
const imageUrl = row.imageUrl.trim() || undefined
|
||||
const name =
|
||||
row.name.trim() ||
|
||||
(index === 0 ? form.color.trim() : '') ||
|
||||
(imageUrl ? `رنگ ${index + 1}` : '')
|
||||
const name = row.name.trim() || (imageUrl ? `رنگ ${index + 1}` : '')
|
||||
if (!name && !imageUrl) return
|
||||
if (!name) return
|
||||
colorEntries.push({
|
||||
@@ -270,7 +260,7 @@ function closeModal() {
|
||||
|
||||
async function submitProduct() {
|
||||
if (!isFormValid.value) {
|
||||
formError.value = 'نام محصول و حداقل متراژ الزامی هستند.'
|
||||
formError.value = 'نام محصول و متراژ هر طاقه الزامی هستند.'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -284,32 +274,39 @@ async function submitProduct() {
|
||||
}
|
||||
|
||||
const { colorEntries, imageUrls } = buildProductMedia()
|
||||
const rollMeters = form.rollMeters
|
||||
const catalogPayload = {
|
||||
usageCategory: form.usageCategory,
|
||||
season: form.season,
|
||||
composition: form.composition.trim() || null,
|
||||
weaveType: form.weaveType.trim() || null,
|
||||
subtitle: form.subtitle.trim() || null,
|
||||
specs: form.specs.trim() || undefined,
|
||||
rollMeters: form.rollMeters,
|
||||
rollMeters,
|
||||
widthCm: form.widthCm,
|
||||
weightGrams: form.weightGrams,
|
||||
isNew: form.isNew,
|
||||
colorsJson: colorEntries,
|
||||
imageUrlsJson: imageUrls,
|
||||
}
|
||||
const productCode = form.productCode.trim() || `PRD-${Date.now()}`
|
||||
const colorLabel = colorEntries[0]?.name || '—'
|
||||
const meteragePayload = {
|
||||
minMeterage: rollMeters,
|
||||
orderStep: rollMeters,
|
||||
orderMultiplier: rollMeters,
|
||||
}
|
||||
const stockPayload = { internalStock: form.inStock ? null : 0 }
|
||||
saving.value = true
|
||||
try {
|
||||
if (modalMode.value === 'edit' && editingId.value) {
|
||||
await productsApi.update(editingId.value, {
|
||||
companyId: companyIdPayload,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || colorEntries[0]?.name || '—',
|
||||
color: colorLabel,
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
orderStep: form.orderStep,
|
||||
price: form.price,
|
||||
internalStock: form.internalStock,
|
||||
...meteragePayload,
|
||||
...stockPayload,
|
||||
isActive: form.isActive,
|
||||
...catalogPayload,
|
||||
})
|
||||
@@ -321,13 +318,11 @@ async function submitProduct() {
|
||||
const created = await productsApi.create({
|
||||
companyId: companyIdPayload,
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim() || colorEntries[0]?.name || '—',
|
||||
color: colorLabel,
|
||||
code: productCode,
|
||||
productCode,
|
||||
minMeterage: form.minMeterage,
|
||||
orderStep: form.orderStep,
|
||||
price: form.price,
|
||||
internalStock: form.internalStock,
|
||||
...meteragePayload,
|
||||
...stockPayload,
|
||||
isActive: form.isActive,
|
||||
...catalogPayload,
|
||||
})
|
||||
@@ -524,120 +519,9 @@ function clearFilters() {
|
||||
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>شرکت مرتبط با محصول</label>
|
||||
<AdminSelect
|
||||
v-model="form.companyId"
|
||||
block
|
||||
aria-label="انتخاب شرکت مرتبط با محصول"
|
||||
placeholder="نادمی تکس (پیشفرض)"
|
||||
:options="companyFormOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-name">نام محصول <span class="admin-form__required">*</span></label>
|
||||
<input id="product-name" v-model="form.name" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-color">رنگ / نوع</label>
|
||||
<input id="product-color" v-model="form.color" type="text" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-code">کد محصول</label>
|
||||
<input id="product-code" v-model="form.productCode" type="text" class="admin-input" dir="ltr" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-min">حداقل متراژ</label>
|
||||
<input id="product-min" v-model.number="form.minMeterage" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-step">ضریب سفارش</label>
|
||||
<input id="product-step" v-model.number="form.orderStep" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-price">قیمت هر متر (تومان)</label>
|
||||
<AdminCurrencyInput id="product-price" v-model="form.price" placeholder="مثلاً ۲۵۰٬۰۰۰" />
|
||||
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-stock">موجودی داخلی (متر)</label>
|
||||
<input id="product-stock" v-model.number="form.internalStock" type="number" min="0" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-usage">کاربرد</label>
|
||||
<AdminSelect
|
||||
id="product-usage"
|
||||
v-model="form.usageCategory"
|
||||
block
|
||||
aria-label="کاربرد محصول"
|
||||
placeholder="انتخاب کاربرد"
|
||||
:options="usageOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-season">فصل</label>
|
||||
<AdminSelect
|
||||
id="product-season"
|
||||
v-model="form.season"
|
||||
block
|
||||
aria-label="فصل محصول"
|
||||
placeholder="انتخاب فصل"
|
||||
:options="seasonOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-composition">جنس / ترکیب</label>
|
||||
<input id="product-composition" v-model="form.composition" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-weave">نوع بافت</label>
|
||||
<input id="product-weave" v-model="form.weaveType" type="text" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-subtitle">زیرعنوان</label>
|
||||
<input id="product-subtitle" v-model="form.subtitle" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-roll">متراژ طاقه</label>
|
||||
<input id="product-roll" v-model.number="form.rollMeters" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-specs">مشخصات محصول</label>
|
||||
<p class="admin-form__hint">
|
||||
متن آزاد؛ در بخش «مشاهده همه مشخصات» صفحه محصول در اپ نمایش داده میشود.
|
||||
</p>
|
||||
<textarea
|
||||
id="product-specs"
|
||||
v-model="form.specs"
|
||||
class="admin-form__textarea"
|
||||
rows="5"
|
||||
placeholder="مثلاً جنس، عرض، وزن و نکات نگهداری — هر خط یک بند"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>تصاویر عمومی محصول</label>
|
||||
<label>عکس اسلایدی محصول</label>
|
||||
<p class="color-variants__hint">
|
||||
فقط برای گالری اصلی در اپ کاربر (بدون وابستگی به رنگ). با «ثبت تغییرات» ثبت میشود.
|
||||
تصاویر گالری اصلی در اپ کاربر. با «ثبت محصول» یا «ثبت تغییرات» ذخیره میشود.
|
||||
</p>
|
||||
<div class="general-images">
|
||||
<div
|
||||
@@ -645,7 +529,7 @@ function clearFilters() {
|
||||
:key="`general-${gIdx}`"
|
||||
class="product-media-row"
|
||||
role="group"
|
||||
:aria-label="`تصویر عمومی ${gIdx + 1}`"
|
||||
:aria-label="`تصویر اسلاید ${gIdx + 1}`"
|
||||
>
|
||||
<AdminImagePicker
|
||||
compact
|
||||
@@ -664,15 +548,75 @@ function clearFilters() {
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="admin-btn admin-btn--outline product-media-add" @click="addGeneralImageSlot">
|
||||
+ افزودن تصویر عمومی
|
||||
+ افزودن تصویر اسلاید
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>رنگبندیها</label>
|
||||
<label for="product-name">نام محصول <span class="admin-form__required">*</span></label>
|
||||
<input id="product-name" v-model="form.name" type="text" class="admin-input" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-code">کد محصول</label>
|
||||
<input id="product-code" v-model="form.productCode" type="text" class="admin-input" dir="ltr" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-width">عرض (سانتیمتر)</label>
|
||||
<input
|
||||
id="product-width"
|
||||
v-model.number="form.widthCm"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
class="admin-input"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-weight">وزن (گرم)</label>
|
||||
<input
|
||||
id="product-weight"
|
||||
v-model.number="form.weightGrams"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
class="admin-input"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-composition">جنس</label>
|
||||
<input id="product-composition" v-model="form.composition" type="text" class="admin-input" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-weave">نوع بافت</label>
|
||||
<input id="product-weave" v-model="form.weaveType" type="text" class="admin-input" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-specs">توضیح اضافه و تکمیلی برای مشخصه فنی</label>
|
||||
<p class="admin-form__hint">
|
||||
در بخش «مشاهده همه مشخصات» صفحه محصول در اپ نمایش داده میشود.
|
||||
</p>
|
||||
<textarea
|
||||
id="product-specs"
|
||||
v-model="form.specs"
|
||||
class="admin-form__textarea"
|
||||
rows="5"
|
||||
placeholder="نکات تکمیلی مشخصات فنی — هر خط یک بند"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>رنگبندی</label>
|
||||
<p class="color-variants__hint">
|
||||
برای هر رنگ یک نام و یک تصویر بگذارید. تصویر رنگ در بخش رنگبندی اپ نمایش داده میشود، نه در گالری عمومی.
|
||||
برای هر رنگ یک نام و یک تصویر بگذارید. تصویر رنگ در بخش رنگبندی اپ نمایش داده میشود.
|
||||
</p>
|
||||
<div class="color-variants">
|
||||
<div
|
||||
@@ -711,12 +655,61 @@ function clearFilters() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="product-usage">دستهبندی</label>
|
||||
<AdminSelect
|
||||
id="product-usage"
|
||||
v-model="form.usageCategory"
|
||||
block
|
||||
aria-label="دستهبندی محصول"
|
||||
placeholder="انتخاب دستهبندی"
|
||||
:options="usageOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="product-season">فصل</label>
|
||||
<AdminSelect
|
||||
id="product-season"
|
||||
v-model="form.season"
|
||||
block
|
||||
aria-label="فصل محصول"
|
||||
placeholder="انتخاب فصل"
|
||||
:options="seasonOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="product-roll">متراژ هر طاقه <span class="admin-form__required">*</span></label>
|
||||
<p class="admin-form__hint">
|
||||
حداقل متراژ و ضریب سفارش بهصورت خودکار برابر با همین مقدار تنظیم میشود.
|
||||
</p>
|
||||
<input id="product-roll" v-model.number="form.rollMeters" type="number" min="1" class="admin-input" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.inStock" label="موجود در انبار" />
|
||||
<p class="admin-form__hint">در صورت اتمام موجودی، خاموش کنید؛ نیازی به ثبت متراژ دقیق نیست.</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label>شرکت مرتبط با محصول</label>
|
||||
<AdminSelect
|
||||
v-model="form.companyId"
|
||||
block
|
||||
aria-label="انتخاب شرکت مرتبط با محصول"
|
||||
placeholder="نادمی تکس (پیشفرض)"
|
||||
:options="companyFormOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.isNew" label="محصول جدید" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.isActive" label="وضعیت" />
|
||||
<BaseSwitch v-model="form.isActive" label="فعال در فروشگاه" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user