hesabixCore/webUI/src/components/plugins/warranty/SerialDialog.vue

356 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-dialog v-model="dialog" max-width="640px" persistent>
<v-card>
<v-card-title class="d-flex align-center p-3 gap-2">
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
<span>{{ isEdit ? 'ویرایش سریال گارانتی' : 'افزودن سریال گارانتی جدید' }}</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-row>
<v-col cols="12" md="6">
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
<template #prepend>
<v-tooltip bottom size="small">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="primary" @click.stop="showQrScanner = true">mdi-barcode-scan</v-icon>
</template>
<span>اسکن بارکد</span>
</v-tooltip>
</template>
</v-text-field>
</v-col>
<v-col cols="12" md="6">
<Hcommoditysearch :model-value="formData.commodity_id ?? undefined"
@update:modelValue="(val: number | Record<string, any>) => { formData.commodity_id = typeof val === 'number' ? val : (val as any)?.id ?? null }"
:return-object="false" label="محصول *" :rules="[rules.commodity]" required class="serial-commodity" />
</v-col>
<v-col cols="12" md="6">
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]"
:ignore-year-range="true" dense outlined hide-details="auto" />
</v-col>
<v-col cols="12" md="6">
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense
outlined hide-details="auto" />
</v-col>
<v-col cols="12" md="6">
<v-select v-model="formData.status" label="وضعیت" :items="statusOptions" item-title="title"
item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
</v-col>
<v-col v-if="isEdit" cols="12" md="6">
<v-select v-model="formData.activation" label="وضعیت فعال‌سازی" :items="activationOptions"
item-title="title" item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
</v-col>
<v-col cols="12">
<v-textarea v-model="formData.description" label="توضیحات" rows="3" auto-grow variant="outlined"
density="comfortable" hide-details="auto" />
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="grey" @click="close">انصراف</v-btn>
<v-btn color="primary" @click="save" :loading="loading" :disabled="!valid">
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
</v-btn>
</v-card-actions>
</v-card>
<BarcodeScanner v-model="showQrScanner" @detected="handleBarcodeScan" />
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
{{ notificationText }}
<template #actions>
<v-btn color="white" text @click="showNotification = false">بستن</v-btn>
</template>
</v-snackbar>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
const props = defineProps<{
modelValue: boolean
serial?: any
commodities: any[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
save: [data: any]
close: []
}>()
const loading = ref(false)
const valid = ref(false)
const form = ref()
const commodityModel = ref<any>(null)
const formData = ref({
serialNumber: '',
commodity_id: null as number | null,
description: '',
warrantyStartDate: '',
warrantyEndDate: '',
status: 'available',
activation: 'deactive',
notes: ''
})
const rules = {
required: (v: any) => !!v || 'این فیلد الزامی است',
serialNumber: (v: any) => {
if (!v) return 'شماره سریال الزامی است'
if (!/^[A-Za-z0-9\-_.]+$/.test(v)) return 'شماره سریال نامعتبر است'
if (v.length < 3) return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
if (v.length > 50) return 'شماره سریال نمی‌تواند بیشتر از ۵۰ کاراکتر باشد'
return true
},
commodity: (v: any) => !!v || 'انتخاب محصول الزامی است',
date: (v: any) => {
if (!v) return true
const d = new Date(v)
return !isNaN(d.getTime()) || 'تاریخ نامعتبر است'
},
endDate: (v: any, s: any) => {
if (!v || !s) return true
const end = new Date(v); const start = new Date(s)
return end > start || 'تاریخ پایان باید بعد از تاریخ شروع باشد'
}
}
const statusOptions = [
{ title: 'آزاد', value: 'available' },
{ title: 'تخصیص یافته', value: 'allocated' },
{ title: 'تأیید شده', value: 'verified' },
{ title: 'متصل', value: 'bound' },
{ title: 'مصرف شده', value: 'consumed' },
{ title: 'باطل', value: 'void' }
]
const activationOptions = [
{ title: 'غیرفعال', value: 'deactive' },
{ title: 'فعال', value: 'active' }
]
const dialog = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const isEdit = computed(() => !!props.serial)
const isMobile = computed(() => window.innerWidth <= 768)
const showNotification = ref(false)
const notificationText = ref('')
const notificationColor = ref<'success' | 'error' | 'warning' | 'info'>('success')
const showNotify = (t: string, c: 'success' | 'error' | 'warning' | 'info' = 'success') => {
notificationText.value = t
notificationColor.value = c
showNotification.value = true
}
const showQrScanner = ref(false)
const save = async () => {
const res = await form.value?.validate()
const ok = typeof res === 'object' ? res.valid : !!res
if (!ok) return
if (!formData.value.serialNumber || !formData.value.commodity_id) return
try {
loading.value = true
emit('save', { ...formData.value })
} finally {
loading.value = false
}
}
const close = () => {
emit('close')
}
const resetForm = () => {
formData.value = {
serialNumber: '',
commodity_id: null,
description: '',
warrantyStartDate: '',
warrantyEndDate: '',
status: 'available',
activation: 'deactive',
notes: ''
}
commodityModel.value = null
form.value?.resetValidation()
}
const loadSerialData = () => {
if (props.serial) {
formData.value = {
serialNumber: props.serial.serialNumber || '',
commodity_id: Number(props.serial.commodity?.id) || null,
description: props.serial.description || '',
warrantyStartDate: props.serial.warrantyStartDate || '',
warrantyEndDate: props.serial.warrantyEndDate || '',
status: props.serial.status || 'available',
activation: props.serial.activation || 'deactive',
notes: props.serial.notes || ''
}
commodityModel.value = props.commodities.find(c => c.id === formData.value.commodity_id) || null
} else {
resetForm()
}
}
const customFilter = (item: any, q: string) => {
const t = String(item?.name || '').toLowerCase()
const s = String(q || '').toLowerCase()
return t.indexOf(s) > -1
}
const handleCommoditySelect = (c: any) => {
formData.value.commodity_id = c?.id ? Number(c.id) : null
}
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
const handleBarcodeScan = (val: string) => {
formData.value.serialNumber = val
showQrScanner.value = false
}
</script>
<style>
/* normalize Hcommoditysearch height with other inputs */
.serial-commodity :deep(.v-field) {
min-height: 56px;
}
.serial-commodity :deep(.v-field__input) {
padding-top: 14px;
padding-bottom: 14px;
}
#qr-shaded-region {
display: none !important;
}
video {
border-radius: 12px;
padding: 2px;
}
.v-dialog {
direction: rtl
}
.qr-card {
max-width: 95vw;
border-radius: 16px
}
.qr-title {
text-align: center;
padding: 14px 16px
}
.qr-wrap {
position: relative;
width: 100%;
max-width: 560px;
margin: 0 auto
}
.qr-reader {
width: 100%;
min-height: 320px;
border-radius: 12px;
border: 1px solid #e5e7eb;
overflow: hidden;
background: #000;
}
/* ویدئو تولیدی کتابخانه */
:deep(#reader video) {
width: 100% !important;
height: auto !important;
display: block !important;
object-fit: cover;
min-height: 240px;
}
/* کانتینر داخلی کتابخانه راست به چپ نشه */
:deep(#reader div) {
direction: ltr
}
/* وضعیت‌ها */
.qr-status {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 12px
}
.qr-actions {
padding: 12px 16px
}
/* ریسپانسیو */
@media (max-width:1024px) {
.qr-reader {
min-height: 300px
}
:deep(#reader video) {
min-height: 220px
}
}
@media (max-width:768px) {
.qr-card {
max-width: 95vw
}
.qr-reader {
min-height: 260px
}
:deep(#reader video) {
min-height: 200px
}
}
@media (max-width:480px) {
.qr-reader {
min-height: 220px
}
:deep(#reader video) {
min-height: 180px
}
}
.v-input--density-compact .v-field--variant-outlined {
height: 3rem !important;
}
.mdi-barcode-scan::before {
font-size: 25px !important;
}
</style>