540 lines
22 KiB
Vue
540 lines
22 KiB
Vue
<script setup>
|
|
import { Listbox, InputText } from 'primevue';
|
|
import { onBeforeMount, ref, watch } from 'vue';
|
|
import { CategoryService } from '@/service/CategoryService';
|
|
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
|
import ImageGallery from '@/views/uikit/ImageGallery.vue';
|
|
import KiwiChips from '@/views/uikit/KiwiChips.vue';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { BelongingService } from '@/service/BelongingService';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import MapComponent from '@/views/uikit/MapComponent.vue';
|
|
import TransactionDialog from './TransactionDialog.vue';
|
|
import { TransactionService } from '@/service/TransactionService';
|
|
import { FilterMatchMode, FilterOperator } from '@primevue/core/api';
|
|
import Popover from 'primevue/popover';
|
|
|
|
// For the single popover approach
|
|
const op = ref();
|
|
const activeDescription = ref('');
|
|
|
|
const toggle = (event, data) => {
|
|
activeDescription.value = data;
|
|
op.value.toggle(event);
|
|
};
|
|
|
|
const toast = useToast();
|
|
const categories = ref([]);
|
|
const selectedCategory = ref();
|
|
const features = ref([]);
|
|
const featuresValues = ref({});
|
|
const documentData = ref([]);
|
|
const mainImageUrlsWithData = ref([]);
|
|
const uploadedMainImageUrls = ref([]);
|
|
const documentUrlsWithData = ref([]);
|
|
const uploadedDocumentUrls = ref([]);
|
|
const editMode = ref(false);
|
|
const belonging = ref({
|
|
location: {
|
|
type: 'Point',
|
|
coordinates: [0, 0]
|
|
}
|
|
});
|
|
const tags = ref([]);
|
|
const signedImageUrls = ref([]);
|
|
const signedDocumentsUrls = ref([]);
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const mapKey = 'web.79abbab4970e401781f434da3e41cfa6';
|
|
const serviceKey = 'service.11d860f52c434bd29e5f84d0a0a41108';
|
|
|
|
const transactions = ref([]);
|
|
const stockData = ref({});
|
|
|
|
const handleDocumentUpload = (newDocumentUrls) => {
|
|
uploadedDocumentUrls.value = newDocumentUrls.value;
|
|
const tempConcatImages = [...belonging.value?.documentUrls, ...newDocumentUrls.value];
|
|
documentUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
const handleDocumentDelete = (newDocumentUrls) => {
|
|
uploadedDocumentUrls.value = newDocumentUrls;
|
|
const tempConcatImages = [...belonging.value?.documentUrls, ...newDocumentUrls.value];
|
|
documentUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
const handleDocumentGalleryDelete = (deletedDocumentUrl, index) => {
|
|
console.log('deletedImageUrl', deletedDocumentUrl, 'at index', index);
|
|
|
|
// Find which array contains the image to delete
|
|
if (index < belonging.value.documentUrls.length) {
|
|
// Remove only at specific index from belonging.value.mainImageNames
|
|
belonging.value.documentUrls.splice(index, 1);
|
|
} else {
|
|
// Adjust index for uploadedMainImageNames array
|
|
const adjustedIndex = index - belonging.value?.documentUrls.length;
|
|
uploadedDocumentUrls.value.splice(adjustedIndex, 1);
|
|
}
|
|
|
|
// Recombine arrays
|
|
const tempConcatImages = [...belonging.value?.documentUrls, ...uploadedDocumentUrls.value];
|
|
documentUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
const handleImageUpload = (newImageUrls) => {
|
|
uploadedMainImageUrls.value = newImageUrls.value;
|
|
const tempConcatImages = [...belonging.value?.mainImageUrls, ...newImageUrls.value];
|
|
mainImageUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
const handleImageDelete = (newImageUrls) => {
|
|
uploadedMainImageUrls.value = newImageUrls;
|
|
const tempConcatImages = [...belonging.value?.mainImageUrls.value, ...newImageUrls.value];
|
|
mainImageUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
const handleImageGalleryDelete = (deletedImageUrl, index) => {
|
|
console.log('deletedImageUrl', deletedImageUrl, 'at index', index);
|
|
|
|
// Find which array contains the image to delete
|
|
if (index < belonging.value?.mainImageUrls.length) {
|
|
// Remove only at specific index from belonging.value.mainImageNames
|
|
belonging.value?.mainImageUrls.splice(index, 1);
|
|
} else {
|
|
// Adjust index for uploadedMainImageNames array
|
|
const adjustedIndex = index - belonging.value?.mainImageUrls.length;
|
|
uploadedMainImageUrls.value?.splice(adjustedIndex, 1);
|
|
}
|
|
|
|
// Recombine arrays
|
|
const tempConcatImages = [...belonging.value?.mainImageUrls, ...uploadedMainImageUrls.value];
|
|
mainImageUrlsWithData.value = fillImagesArray(tempConcatImages);
|
|
};
|
|
|
|
onBeforeMount(async () => {
|
|
categories.value = await CategoryService.getAllCategories();
|
|
console.log(route.name);
|
|
if (route.name === 'edit-belonging') {
|
|
editMode.value = true;
|
|
belonging.value = await BelongingService.getBelonging(route.params.belongingId);
|
|
featuresValues.value = extractFeaturesValues(belonging.value.features);
|
|
transactions.value = await TransactionService.getTransactionsByBelongingId(route.params.belongingId);
|
|
stockData.value = await TransactionService.getStockByBelongingId(route.params.belongingId);
|
|
mainImageUrlsWithData.value = fillImagesArray(belonging.value?.mainImageUrls || []);
|
|
documentUrlsWithData.value = fillImagesArray(belonging.value?.documentUrls || []);
|
|
}
|
|
});
|
|
const showTransactionDialog = ref(false);
|
|
|
|
import { UTMtoLatLon, LatLontoUTM } from '@/utm';
|
|
|
|
const easting = ref();
|
|
const northing = ref();
|
|
const zone = ref();
|
|
const lat = ref();
|
|
const long = ref();
|
|
|
|
const convertUTMtoLatLng = () => {
|
|
if (easting.value && northing.value && zone.value) {
|
|
try {
|
|
const result = UTMtoLatLon(
|
|
Number(easting.value),
|
|
Number(northing.value),
|
|
Number(zone.value),
|
|
'N' // Change to 'S' if in Southern Hemisphere
|
|
);
|
|
lat.value = result.latitude;
|
|
long.value = result.longitude;
|
|
console.log(lat.value, long.value);
|
|
searchLocation(lat.value, long.value);
|
|
} catch (error) {
|
|
console.error('Error converting UTM to Lat/Lng:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Watch nationalGeoCode to split and convert to Lat/Lng
|
|
watch(
|
|
() => belonging.value.nationalGeoCode,
|
|
(newCode) => {
|
|
if (newCode && newCode.length === 15) {
|
|
easting.value = newCode.substring(0, 6); // First 6 digits for easting
|
|
northing.value = newCode.substring(6, 13); // Next 6 digits for northing
|
|
zone.value = newCode.substring(13, 15); // Last 2 digits for zone
|
|
// Convert to Lat/Lng after splitting
|
|
convertUTMtoLatLng();
|
|
}
|
|
}
|
|
);
|
|
|
|
const mapRef = ref();
|
|
|
|
const searchLocation = (lat, lon) => {
|
|
if (mapRef.value) {
|
|
mapRef.value.searchByCoordinates(lat, lon);
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => belonging.value.category,
|
|
(newCategory) => {
|
|
if (editMode.value && newCategory) {
|
|
selectedCategory.value = categories.value.find((x) => x._id === newCategory);
|
|
if (selectedCategory.value) {
|
|
features.value = selectedCategory.value.features;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
const fillImagesArray = (imageUrls) => {
|
|
const images = [];
|
|
for (let url of imageUrls) {
|
|
images.push({ url: url });
|
|
}
|
|
return images;
|
|
};
|
|
|
|
const openTransactionDialog = () => {
|
|
showTransactionDialog.value = true;
|
|
};
|
|
|
|
const onTransactionSaved = async () => {
|
|
if (editMode.value && belonging.value?._id) {
|
|
belonging.value = await BelongingService.getBelonging(belonging.value._id);
|
|
}
|
|
// toast.add({ severity: 'success', summary: 'موفق', detail: 'تراکنش با موفقیت ثبت شد', life: 3000 });
|
|
};
|
|
|
|
const handleSelectCategory = ($event) => {
|
|
if ($event.value) {
|
|
belonging.value.category = $event.value;
|
|
selectedCategory.value = categories.value.find((x) => x._id === $event.value);
|
|
features.value = selectedCategory.value.features;
|
|
} else {
|
|
belonging.value = {
|
|
location: {
|
|
type: 'Point',
|
|
coordinates: [0, 0]
|
|
}
|
|
};
|
|
selectedCategory.value = {};
|
|
}
|
|
};
|
|
|
|
const extractFeatures = (featuresValues) => {
|
|
const features = [];
|
|
|
|
for (const [key, value] of Object.entries(featuresValues)) {
|
|
features.push({
|
|
definition: key,
|
|
value: value
|
|
});
|
|
}
|
|
|
|
return features;
|
|
};
|
|
|
|
const extractFeaturesValues = (features) => {
|
|
const featuresValues = {};
|
|
|
|
for (let feature of features) {
|
|
featuresValues[feature.definition] = feature.value;
|
|
}
|
|
|
|
return featuresValues;
|
|
};
|
|
|
|
const saveBelonging = async () => {
|
|
let status = 'failed';
|
|
console.log(belonging);
|
|
if (belonging?.value._id?.trim()) {
|
|
belonging.value.mainImageUrls = mainImageUrlsWithData.value.map((x) => x.url);
|
|
belonging.value.documentUrls = documentUrlsWithData.value.map((x) => x.url);
|
|
belonging.value.features = extractFeatures(featuresValues.value);
|
|
status = await BelongingService.updateBelonging(belonging.value._id, belonging.value);
|
|
} else {
|
|
status = await BelongingService.createBelonging(belonging.value);
|
|
}
|
|
|
|
if (status === 'success') {
|
|
toast.add({ severity: 'success', summary: 'موفق', detail: 'ویژگی جدید ساخته شد.', life: 3000 });
|
|
} else {
|
|
toast.add({ severity: 'error', summary: 'ناموفق', detail: status, life: 3000 });
|
|
}
|
|
|
|
await router.push('/belonging/list-belongings');
|
|
};
|
|
|
|
const filters = ref({
|
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
|
type: { value: null, matchMode: FilterMatchMode.EQUALS },
|
|
status: { value: null, matchMode: FilterMatchMode.EQUALS },
|
|
value: { value: null, matchMode: FilterMatchMode.BETWEEN },
|
|
date: { value: null, matchMode: FilterMatchMode.DATE_IS }
|
|
});
|
|
|
|
// Options for filters
|
|
const typeOptions = [
|
|
{ label: 'واریز', value: 'deposit' },
|
|
{ label: 'برداشت', value: 'withdraw' }
|
|
];
|
|
|
|
const statusOptions = [
|
|
{ label: 'ناموفق', value: 'failed' },
|
|
{ label: 'در انتظار', value: 'pending' },
|
|
{ label: 'تکمیل شده', value: 'finished' }
|
|
];
|
|
|
|
// Helper functions for translations and formatting
|
|
const getTypeTranslation = (type) => {
|
|
switch (type) {
|
|
case 'deposit':
|
|
return 'واریز';
|
|
case 'withdraw':
|
|
return 'برداشت';
|
|
default:
|
|
return type;
|
|
}
|
|
};
|
|
|
|
const getTypeSeverity = (type) => {
|
|
switch (type) {
|
|
case 'deposit':
|
|
return 'success';
|
|
case 'withdraw':
|
|
return 'danger';
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getStatusTranslation = (status) => {
|
|
switch (status) {
|
|
case 'failed':
|
|
return 'ناموفق';
|
|
case 'pending':
|
|
return 'در انتظار';
|
|
case 'finished':
|
|
return 'تکمیل شده';
|
|
default:
|
|
return status;
|
|
}
|
|
};
|
|
|
|
const getStatusSeverity = (status) => {
|
|
switch (status) {
|
|
case 'failed':
|
|
return 'danger';
|
|
case 'pending':
|
|
return 'warn';
|
|
case 'finished':
|
|
return 'success';
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (value) => {
|
|
return new Intl.NumberFormat('fa-IR', {
|
|
style: 'currency',
|
|
currency: 'IRR',
|
|
minimumFractionDigits: 0
|
|
}).format(Math.abs(value));
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat('fa-IR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(date);
|
|
};
|
|
</script>
|
|
<template>
|
|
<TransactionDialog v-model:visible="showTransactionDialog" :belongingId="belonging._id" @saved="onTransactionSaved" />
|
|
<div className="card lg:p-5 px-3">
|
|
<div class="mb-4" v-if="editMode === true">
|
|
<Button @click="openTransactionDialog">افزایش یا کاهش موجودی</Button>
|
|
</div>
|
|
<div class="font-semibold text-xl mb-4" v-if="editMode === false">ایجاد اموال</div>
|
|
<p v-if="editMode === false">از طریق این صفحه می توانید اموال جدید را به سوابق اضافه کنید.</p>
|
|
<div class="font-semibold text-xl mb-4" v-if="editMode === true">
|
|
در حال ویرایش
|
|
<span class="text-primary">{{ belonging.name }}</span>
|
|
</div>
|
|
|
|
<div class="lg:flex my-6">
|
|
<div class="lg:w-3/4 w-full ml-3">
|
|
<div class="">
|
|
<label class="text-lg"> نام دارایی </label>
|
|
<InputText v-model="belonging.name" class="w-full my-2" placeholder="مثلا: 100 عدد کولر گازی" />
|
|
</div>
|
|
<div class="my-6">
|
|
<label class="text-lg"> توضیحات </label>
|
|
<Textarea v-model="belonging.description" class="w-full my-2" placeholder="مثلا: در تاریخ 1403/09/11 خریداری شد." rows="6" />
|
|
</div>
|
|
<div class="my-6">
|
|
<label class="text-lg"> تصاویر، فایل ها و مدارک </label>
|
|
<ImageUploader
|
|
v-model="uploadedDocumentUrls"
|
|
:max-files="50"
|
|
mode="private"
|
|
delete-endpoint="/api/files/bucket/delete-file/belonging-documents"
|
|
upload-endpoint="/api/files/bucket/upload-multi-filepond/belonging-documents"
|
|
@upload="handleDocumentUpload"
|
|
@delete="handleDocumentDelete"
|
|
/>
|
|
<div v-if="documentUrlsWithData.length > 0">
|
|
<ImageGallery v-model="documentUrlsWithData" gallery-id="documents" @delete-image="handleDocumentGalleryDelete" />
|
|
</div>
|
|
</div>
|
|
<div class="my-6" v-if="signedDocumentsUrls.length > 0">
|
|
<label class="text-lg"> فایل ها و مدارک </label>
|
|
</div>
|
|
<div class="my-6 max-h-96 overflow-y-auto" v-if="features.length > 0">
|
|
<label class="text-lg"> اطلاعات دارایی </label>
|
|
<div class="my-6 mr-10" v-for="feature of features">
|
|
<label>{{ feature.nativeName }}</label>
|
|
<InputText v-model="featuresValues[feature._id]" class="w-1/3 my-2 block" placeholder="" />
|
|
</div>
|
|
</div>
|
|
<div class="my-10">
|
|
<label class="text-lg"> موقعیت دارایی </label>
|
|
<MapComponent ref="mapRef" v-model="belonging.location.coordinates" />
|
|
<!-- <MapIrComponent />-->
|
|
</div>
|
|
|
|
<div class="my-10" v-if="editMode">
|
|
<label class="text-lg"> موجودی </label>
|
|
<p class="mx-3 text-blue-400">موجودی فعلی : {{ stockData.finishedInventory }}</p>
|
|
<p class="mx-3 text-green-400">موجودی در حال اضافه شدن : {{ stockData.pendingDepositsInventory }}</p>
|
|
<p class="mx-3 text-red-400">
|
|
موجودی در حال کم شدن : <span dir="ltr">{{ stockData.pendingWithdrawsInventory }}</span>
|
|
</p>
|
|
<div class="mx-6 py-4">
|
|
<label> تراکنش ها </label>
|
|
<div class="card lg:p-5 px-3">
|
|
<DataTable
|
|
:value="transactions"
|
|
paginator
|
|
:rows="20"
|
|
:rowsPerPageOptions="[20, 50, 100]"
|
|
tableStyle="min-width: 50rem"
|
|
stripedRows
|
|
v-model:filters="filters"
|
|
filterDisplay="menu"
|
|
:globalFilterFields="['type', 'status', 'value', 'date', 'description']"
|
|
>
|
|
<template #header>
|
|
<div class="lg:flex lg:justify-content-between">
|
|
<span class="p-input-icon-left">
|
|
<!-- <i class="pi pi-search" />-->
|
|
<InputText v-model="filters['global'].value" placeholder="جستجو..." class="w-full" />
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<Column field="id" header="شناسه" sortable style="width: 25%"></Column>
|
|
|
|
<Column field="type" header="نوع" sortable style="width: 15%">
|
|
<template #body="{ data }">
|
|
<Tag :severity="getTypeSeverity(data.type)" :value="getTypeTranslation(data.type)" />
|
|
</template>
|
|
<template #filter="{ filterModel, filterCallback }">
|
|
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="انتخاب نوع" class="p-column-filter" showClear />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="status" header="وضعیت" sortable style="width: 15%">
|
|
<template #body="{ data }">
|
|
<Tag :severity="getStatusSeverity(data.status)" :value="getStatusTranslation(data.status)" />
|
|
</template>
|
|
<template #filter="{ filterModel, filterCallback }">
|
|
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="انتخاب وضعیت" class="p-column-filter" showClear />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="value" header="تعداد" sortable style="width: 15%">
|
|
<template #body="{ data }">
|
|
<span :class="data.value >= 0 ? 'text-green-500' : 'text-red-500'" dir="ltr"> {{ data.value }} </span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="date" header="تاریخ" sortable style="width: 20%">
|
|
<template #body="{ data }">
|
|
{{ formatDate(data.date) }}
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="description" header="توضیحات" style="width: 10%">
|
|
<template #body="{ data }">
|
|
<Button v-if="data.description" icon="pi pi-info-circle" text rounded aria-label="توضیحات" @click="toggle($event, data.description)" />
|
|
</template>
|
|
</Column>
|
|
<Popover ref="op" :showCloseIcon="true" :autoHide="false">
|
|
<div class="p-2">{{ activeDescription }}</div>
|
|
</Popover>
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="lg:w-1/4 w-full">
|
|
<div>
|
|
<label class="text-lg"> دسته بندی </label>
|
|
<Listbox @change="handleSelectCategory($event)" v-model="belonging.category" :options="categories" optionLabel="nativeName" optionValue="_id" checkmark :highlightOnSelect="false" class="w-full my-2" />
|
|
</div>
|
|
<div class="my-6">
|
|
<label class="text-lg"> تصویر اصلی </label>
|
|
<ImageUploader
|
|
v-model="uploadedMainImageUrls"
|
|
accepted-files="image/*"
|
|
:max-files="1"
|
|
mode="private"
|
|
delete-endpoint="/api/files/bucket/delete/belonging-images"
|
|
upload-endpoint="/api/files/bucket/upload-multi-filepond/belonging-images"
|
|
@upload="handleImageUpload"
|
|
@delete="handleImageDelete"
|
|
/>
|
|
<div class="my-10 border rounded-lg p-3 border-surface-700" v-if="mainImageUrlsWithData.length > 0">
|
|
<label for="ImageGallery">گالری تصاویر</label>
|
|
<div class="">
|
|
<ImageGallery v-model="mainImageUrlsWithData" gallery-id="productImages" @delete-image="handleImageGalleryDelete" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="my-8">
|
|
<label class="text-lg"> واحد </label>
|
|
<InputText v-model="belonging.unit" class="w-full" placeholder="مثلا: عدد، متر، و..." />
|
|
</div>
|
|
<div class="my-8">
|
|
<label class="text-lg"> برچسب ها </label>
|
|
<KiwiChips v-model="belonging.tags" class="w-full" placeholder="مثلا: زمین، ملک، کیانپارس..." />
|
|
</div>
|
|
|
|
<div class="my-8">
|
|
<label class="text-lg"> شناسه ملی جغرافیایی </label>
|
|
<InputText v-model="belonging.nationalGeoCode" class="w-full" placeholder="شناسه 14 رقمی سند" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<Button @click="saveBelonging">ذخیره</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<style>
|
|
@import url('leaflet/dist/leaflet.css');
|
|
</style>
|
|
|
|
<style scoped>
|
|
label {
|
|
@apply text-primary;
|
|
}
|
|
</style>
|