bug fix
This commit is contained in:
parent
c09fe66a5f
commit
d231e81252
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20241220000000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add memberCount field to chat_channel table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Add memberCount column to chat_channel table
|
||||
$this->addSql('ALTER TABLE chat_channel ADD member_count INT NOT NULL DEFAULT 0');
|
||||
|
||||
// Update existing channels with correct member count
|
||||
$this->addSql('
|
||||
UPDATE chat_channel c
|
||||
SET member_count = (
|
||||
SELECT COUNT(*)
|
||||
FROM chat_channel_member m
|
||||
WHERE m.channel_id = c.id AND m.is_active = 1
|
||||
)
|
||||
');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chat_channel DROP member_count');
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250809100001 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create custom_invoice_template table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE custom_invoice_template (id INT AUTO_INCREMENT NOT NULL, bid_id INT NOT NULL, submitter_id INT NOT NULL, name VARCHAR(255) NOT NULL, is_public TINYINT(1) NOT NULL, code LONGTEXT NOT NULL, INDEX IDX_CUSTOM_INV_TPL_BID (bid_id), INDEX IDX_CUSTOM_INV_TPL_SUBMITTER (submitter_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_BID FOREIGN KEY (bid_id) REFERENCES business (id)');
|
||||
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_SUBMITTER FOREIGN KEY (submitter_id) REFERENCES `user` (id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_BID');
|
||||
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_SUBMITTER');
|
||||
$this->addSql('DROP TABLE custom_invoice_template');
|
||||
}
|
||||
}
|
|
@ -10,22 +10,26 @@ use Doctrine\Migrations\AbstractMigration;
|
|||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250806183700 extends AbstractMigration
|
||||
final class Version20250811113332 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add warehouseManager field to permission table';
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE permission ADD warehouse_manager TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE person ADD require_two_step TINYINT(1) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE permission DROP warehouse_manager');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE person DROP require_two_step
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -288,6 +288,10 @@ class PersonService
|
|||
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
|
||||
if (isset($params['company'])) $person->setCompany($params['company']);
|
||||
if (isset($params['tags'])) $person->setTags($params['tags']);
|
||||
if (isset($params['requireTwoStep'])) {
|
||||
error_log("Setting requireTwoStep: " . var_export($params['requireTwoStep'], true));
|
||||
$person->setRequireTwoStep((bool)$params['requireTwoStep']);
|
||||
}
|
||||
if (array_key_exists('prelabel', $params)) {
|
||||
if ($params['prelabel'] != '') {
|
||||
$prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]);
|
||||
|
|
|
@ -324,7 +324,8 @@ class SellController extends AbstractController
|
|||
|
||||
// Two-step approval: اگر پرمیشن کسبوکار تأیید دو مرحلهای فروش را الزامی کرده باشد
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
if ($permission && $permission->isRequireTwoStepSell()) {
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepSell()) || $personRequire) {
|
||||
$doc->setStatus('pending_approval');
|
||||
} else {
|
||||
$doc->setStatus('approved');
|
||||
|
@ -1320,7 +1321,8 @@ class SellController extends AbstractController
|
|||
|
||||
// Two-step approval برای دریافت/پرداخت
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
if ($permission && $permission->isRequireTwoStepPayment()) {
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepPayment()) || $personRequire) {
|
||||
$paymentDoc->setStatus('pending_approval');
|
||||
} else {
|
||||
$paymentDoc->setStatus('approved');
|
||||
|
|
|
@ -576,7 +576,8 @@ class StoreroomController extends AbstractController
|
|||
$entityManager->flush();
|
||||
// اگر تأیید دو مرحلهای حواله فعال باشد، وضعیت را pending_approval بگذاریم
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
if ($permission && $permission->isRequireTwoStepStore()) {
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepStore()) || $personRequire) {
|
||||
$ticket->setStatus('pending_approval');
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
|
|
|
@ -161,6 +161,9 @@ class Person
|
|||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $tags = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStep = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->hesabdariRows = new ArrayCollection();
|
||||
|
@ -913,4 +916,15 @@ class Person
|
|||
$this->tags = $tags;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStep(): ?bool
|
||||
{
|
||||
return $this->requireTwoStep;
|
||||
}
|
||||
|
||||
public function setRequireTwoStep(?bool $requireTwoStep): self
|
||||
{
|
||||
$this->requireTwoStep = $requireTwoStep;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" max-width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="ml-2">mdi-plus</v-icon>
|
||||
پرونده واردات جدید
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="create" validate-on="input">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.title"
|
||||
label="عنوان پرونده"
|
||||
:rules="[rules.required, rules.minLength]"
|
||||
required
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.supplierName"
|
||||
label="نام تامین کننده"
|
||||
:rules="[rules.required, rules.minLength]"
|
||||
required
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.supplierCountry"
|
||||
label="کشور تامین کننده"
|
||||
:rules="[rules.minLength]"
|
||||
counter="50"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.supplierPhone"
|
||||
label="تلفن تامین کننده"
|
||||
:rules="[rules.phone]"
|
||||
counter="20"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="formData.supplierEmail"
|
||||
label="ایمیل تامین کننده"
|
||||
type="email"
|
||||
:rules="[rules.email]"
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.supplierAddress"
|
||||
label="آدرس تامین کننده"
|
||||
rows="2"
|
||||
:rules="[rules.maxLength]"
|
||||
counter="500"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.totalAmount)"
|
||||
label="مبلغ کل"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney, rules.maxAmount]"
|
||||
@update:modelValue="onMoneyInput('totalAmount', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="formData.currency"
|
||||
:items="currencyOptions"
|
||||
label="واحد پول"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.exchangeRate)"
|
||||
label="نرخ تبدیل"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney, rules.maxExchangeRate]"
|
||||
@update:modelValue="onMoneyInput('exchangeRate', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(formData.totalAmountIRR)"
|
||||
label="مبلغ کل (ریال)"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="3"
|
||||
:rules="[rules.maxLength]"
|
||||
counter="1000"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="cancel">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
ایجاد
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
// Props & Emits
|
||||
const props = defineProps({
|
||||
modelValue: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'created'])
|
||||
|
||||
// Data
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
title: '',
|
||||
supplierName: '',
|
||||
supplierCountry: '',
|
||||
supplierPhone: '',
|
||||
supplierEmail: '',
|
||||
supplierAddress: '',
|
||||
totalAmount: '',
|
||||
currency: 'USD',
|
||||
exchangeRate: '',
|
||||
totalAmountIRR: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// Computed
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// Currency options
|
||||
const currencyOptions = [
|
||||
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
minLength: (value) => !value || value.length >= 2 || 'حداقل 2 کاراکتر الزامی است',
|
||||
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
||||
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
||||
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||
positiveMoney: (value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||
},
|
||||
maxAmount: (value) => !value || parseFloat(value) <= 999999999 || 'مبلغ نباید بیشتر از 999,999,999 باشد',
|
||||
maxExchangeRate: (value) => !value || parseFloat(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد'
|
||||
}
|
||||
|
||||
const parseMoneyInput = (val) => {
|
||||
if (val === null || val === undefined) return 0
|
||||
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||
const num = Number(cleaned)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
}
|
||||
|
||||
const onMoneyInput = (field, value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
formData.value[field] = numeric
|
||||
}
|
||||
|
||||
const formatMoney = (value) => {
|
||||
const numericValue = Number(value) || 0
|
||||
return numericValue
|
||||
.toFixed(0)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
watch([
|
||||
() => formData.value.totalAmount,
|
||||
() => formData.value.exchangeRate,
|
||||
() => formData.value.currency
|
||||
], ([newTotalAmount, newExchangeRate, currency]) => {
|
||||
const total = parseMoneyInput(newTotalAmount)
|
||||
const rate = currency === 'IRR' ? 1 : parseMoneyInput(newExchangeRate)
|
||||
const result = Math.round(total * rate)
|
||||
formData.value.totalAmountIRR = isNaN(result) ? 0 : result
|
||||
}, { immediate: true })
|
||||
|
||||
// Methods
|
||||
const create = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/import-workflow/create', formData.value)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: 'پرونده واردات با موفقیت ایجاد شد',
|
||||
icon: 'success'
|
||||
})
|
||||
resetForm()
|
||||
emit('created')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating workflow:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ایجاد پرونده واردات خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
resetForm()
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
title: '',
|
||||
supplierName: '',
|
||||
supplierCountry: '',
|
||||
supplierPhone: '',
|
||||
supplierEmail: '',
|
||||
supplierAddress: '',
|
||||
totalAmount: '',
|
||||
currency: 'USD',
|
||||
exchangeRate: '',
|
||||
totalAmountIRR: '',
|
||||
description: ''
|
||||
}
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.ltr-input :deep(input) {
|
||||
direction: ltr !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,423 @@
|
|||
<template>
|
||||
<div class="import-workflow-customs">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>اطلاعات ترخیص گمرکی</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن اطلاعات ترخیص
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="customs"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="اطلاعات ترخیص گمرکی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.totalCustomsCharges="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.totalCustomsCharges) }}
|
||||
<small class="text-medium-emphasis">ریال</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editCustoms(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteCustoms(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingCustoms ? 'ویرایش اطلاعات ترخیص' : 'افزودن اطلاعات ترخیص جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveCustoms">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.declarationNumber"
|
||||
label="شماره اظهارنامه"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.customsCode"
|
||||
label="کد گمرک"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.clearanceDate"
|
||||
label="تاریخ ترخیص"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.customsBroker"
|
||||
label="ترخیصکار"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.customsDuty)"
|
||||
label="حقوق گمرکی"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney]"
|
||||
@update:modelValue="onMoneyInput('customsDuty', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.valueAddedTax)"
|
||||
label="مالیات بر ارزش افزوده"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney]"
|
||||
@update:modelValue="onMoneyInput('valueAddedTax', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.otherCharges)"
|
||||
label="سایر عوارض"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney]"
|
||||
@update:modelValue="onMoneyInput('otherCharges', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(totalCharges)"
|
||||
label="کل هزینههای گمرکی"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.warehouseNumber"
|
||||
label="شماره انبار"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.customsBrokerPhone"
|
||||
label="تلفن ترخیصکار"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.customsBrokerEmail"
|
||||
label="ایمیل ترخیصکار"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="formData.warehouseLocation"
|
||||
label="محل انبار"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saveLoading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingCustoms ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
customs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const editingCustoms = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
declarationNumber: '',
|
||||
customsCode: '',
|
||||
clearanceDate: '',
|
||||
customsDuty: '',
|
||||
valueAddedTax: '',
|
||||
otherCharges: '',
|
||||
totalCustomsCharges: '',
|
||||
customsBroker: '',
|
||||
customsBrokerPhone: '',
|
||||
customsBrokerEmail: '',
|
||||
warehouseNumber: '',
|
||||
warehouseLocation: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'شماره اظهارنامه', key: 'declarationNumber', sortable: false },
|
||||
{ title: 'کد گمرک', key: 'customsCode', sortable: false },
|
||||
{ title: 'تاریخ ترخیص', key: 'clearanceDate', sortable: false },
|
||||
{ title: 'ترخیصکار', key: 'customsBroker', sortable: false },
|
||||
{ title: 'کل هزینهها', key: 'totalCustomsCharges', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||
positiveMoney: (value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
return numeric >= 0 || 'مقدار باید مثبت باشد'
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers for money formatting/parse and LTR input
|
||||
const parseMoneyInput = (val) => {
|
||||
if (val === null || val === undefined) return 0
|
||||
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||
const num = Number(cleaned)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
}
|
||||
|
||||
const onMoneyInput = (field, value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
formData.value[field] = numeric
|
||||
}
|
||||
|
||||
const formatMoney = (value) => {
|
||||
const numericValue = Number(value) || 0
|
||||
return numericValue
|
||||
.toFixed(0)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// Computed
|
||||
const totalCharges = computed(() => {
|
||||
const duty = parseFloat(formData.value.customsDuty) || 0
|
||||
const vat = parseFloat(formData.value.valueAddedTax) || 0
|
||||
const other = parseFloat(formData.value.otherCharges) || 0
|
||||
const total = duty + vat + other
|
||||
formData.value.totalCustomsCharges = total.toString()
|
||||
return formatNumber(total)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const editCustoms = (customs) => {
|
||||
editingCustoms.value = customs
|
||||
formData.value = { ...customs }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteCustoms = async (customs) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف اطلاعات ترخیص',
|
||||
text: 'آیا از حذف این اطلاعات ترخیص گمرکی اطمینان دارید؟',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'لغو'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/customs/${customs.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire('موفق', 'اطلاعات ترخیص با موفقیت حذف شد', 'success')
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('خطا', 'در حذف اطلاعات ترخیص خطایی رخ داد', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveCustoms = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const url = editingCustoms.value
|
||||
? `/api/import-workflow/${props.workflowId}/customs/${editingCustoms.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/customs/create`
|
||||
|
||||
const method = editingCustoms.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingCustoms.value ? 'اطلاعات ترخیص با موفقیت ویرایش شد' : 'اطلاعات ترخیص با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
closeDialog()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving customs:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره اطلاعات ترخیص خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
editingCustoms.value = null
|
||||
formData.value = {
|
||||
declarationNumber: '',
|
||||
customsCode: '',
|
||||
clearanceDate: '',
|
||||
customsDuty: '',
|
||||
valueAddedTax: '',
|
||||
otherCharges: '',
|
||||
totalCustomsCharges: '',
|
||||
customsBroker: '',
|
||||
customsBrokerPhone: '',
|
||||
customsBrokerEmail: '',
|
||||
warehouseNumber: '',
|
||||
warehouseLocation: '',
|
||||
description: ''
|
||||
}
|
||||
showAddDialog.value = false
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-customs {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,424 @@
|
|||
<template>
|
||||
<div class="import-workflow-documents">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>مدیریت اسناد</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن سند
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="documents"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="سندی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeColor(item.type)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getTypeText(item.type) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.fileName="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="ml-1">{{ getFileIcon(item.fileType) }}</v-icon>
|
||||
<span>{{ item.fileName || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.fileSize="{ item }">
|
||||
{{ formatFileSize(item.fileSize) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
v-if="item.filePath"
|
||||
icon="mdi-download"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="downloadFile(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editDocument(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteDocument(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="600" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingDocument ? 'ویرایش سند' : 'افزودن سند جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveDocument">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.type"
|
||||
:items="documentTypes"
|
||||
label="نوع سند"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.title"
|
||||
label="عنوان سند"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.documentNumber"
|
||||
label="شماره سند"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.issueDate"
|
||||
label="تاریخ صدور"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
label="فایل سند"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
|
||||
prepend-icon="mdi-paperclip"
|
||||
:rules="editingDocument ? [] : [rules.fileRequired]"
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saveLoading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingDocument ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
documents: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const editingDocument = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const selectedFile = ref([])
|
||||
|
||||
const formData = ref({
|
||||
type: '',
|
||||
title: '',
|
||||
documentNumber: '',
|
||||
issueDate: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'نوع سند', key: 'type', sortable: false },
|
||||
{ title: 'عنوان', key: 'title', sortable: false },
|
||||
{ title: 'فایل', key: 'fileName', sortable: false },
|
||||
{ title: 'حجم', key: 'fileSize', sortable: false },
|
||||
{ title: 'شماره سند', key: 'documentNumber', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Document types
|
||||
const documentTypes = [
|
||||
{ title: 'فاکتور تجاری', value: 'commercial_invoice' },
|
||||
{ title: 'پیش فاکتور', value: 'proforma_invoice' },
|
||||
{ title: 'بارنامه', value: 'bill_of_lading' },
|
||||
{ title: 'لیست بستهبندی', value: 'packing_list' },
|
||||
{ title: 'گواهی مبدا', value: 'certificate_of_origin' },
|
||||
{ title: 'گواهی کیفیت', value: 'quality_certificate' },
|
||||
{ title: 'مجوز واردات', value: 'import_permit' },
|
||||
{ title: 'اظهارنامه گمرکی', value: 'customs_declaration' },
|
||||
{ title: 'رسید پرداخت', value: 'payment_receipt' },
|
||||
{ title: 'سایر اسناد', value: 'other' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
fileRequired: (value) => (value && value.length > 0) || 'انتخاب فایل الزامی است'
|
||||
}
|
||||
|
||||
// Methods
|
||||
const editDocument = (document) => {
|
||||
editingDocument.value = document
|
||||
formData.value = { ...document }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteDocument = async (document) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف سند',
|
||||
text: `آیا از حذف سند "${document.title}" اطمینان دارید؟`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'لغو'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/documents/${document.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire('موفق', 'سند با موفقیت حذف شد', 'success')
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('خطا', 'در حذف سند خطایی رخ داد', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveDocument = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const formDataToSend = new FormData()
|
||||
|
||||
// Add form fields
|
||||
Object.keys(formData.value).forEach(key => {
|
||||
formDataToSend.append(key, formData.value[key] || '')
|
||||
})
|
||||
|
||||
// Add file if selected
|
||||
if (selectedFile.value && selectedFile.value.length > 0) {
|
||||
formDataToSend.append('file', selectedFile.value[0])
|
||||
}
|
||||
|
||||
const url = editingDocument.value
|
||||
? `/api/import-workflow/${props.workflowId}/documents/${editingDocument.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/documents/create`
|
||||
|
||||
const method = editingDocument.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formDataToSend,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingDocument.value ? 'سند با موفقیت ویرایش شد' : 'سند با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
closeDialog()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving document:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره سند خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = (document) => {
|
||||
if (document.filePath) {
|
||||
window.open(`/api/import-workflow/documents/${document.id}/download`, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
editingDocument.value = null
|
||||
selectedFile.value = []
|
||||
formData.value = {
|
||||
type: '',
|
||||
title: '',
|
||||
documentNumber: '',
|
||||
issueDate: '',
|
||||
description: ''
|
||||
}
|
||||
showAddDialog.value = false
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
commercial_invoice: 'blue',
|
||||
proforma_invoice: 'light-blue',
|
||||
bill_of_lading: 'purple',
|
||||
packing_list: 'teal',
|
||||
certificate_of_origin: 'green',
|
||||
quality_certificate: 'lime',
|
||||
import_permit: 'orange',
|
||||
customs_declaration: 'red',
|
||||
payment_receipt: 'pink',
|
||||
other: 'grey'
|
||||
}
|
||||
return colors[type] || 'grey'
|
||||
}
|
||||
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
commercial_invoice: 'فاکتور تجاری',
|
||||
proforma_invoice: 'پیش فاکتور',
|
||||
bill_of_lading: 'بارنامه',
|
||||
packing_list: 'لیست بستهبندی',
|
||||
certificate_of_origin: 'گواهی مبدا',
|
||||
quality_certificate: 'گواهی کیفیت',
|
||||
import_permit: 'مجوز واردات',
|
||||
customs_declaration: 'اظهارنامه گمرکی',
|
||||
payment_receipt: 'رسید پرداخت',
|
||||
other: 'سایر'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
const getFileIcon = (fileType) => {
|
||||
if (!fileType) return 'mdi-file'
|
||||
|
||||
if (fileType.includes('pdf')) return 'mdi-file-pdf-box'
|
||||
if (fileType.includes('image')) return 'mdi-file-image'
|
||||
if (fileType.includes('word')) return 'mdi-file-word'
|
||||
if (fileType.includes('excel')) return 'mdi-file-excel'
|
||||
|
||||
return 'mdi-file'
|
||||
}
|
||||
|
||||
const formatFileSize = (size) => {
|
||||
if (!size) return '-'
|
||||
|
||||
const bytes = parseInt(size)
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-documents {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,484 @@
|
|||
<template>
|
||||
<div class="import-workflow-items">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>آیتمهای وارداتی</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن آیتم
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="آیتمی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.unitPrice="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.unitPrice) }}
|
||||
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.totalPrice="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.totalPrice) }}
|
||||
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editItem(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteItem(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingItem ? 'ویرایش آیتم' : 'افزودن آیتم جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveItem">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.name"
|
||||
label="نام کالا"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.productCode"
|
||||
label="کد محصول"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.brand"
|
||||
label="برند"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.model"
|
||||
label="مدل"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.originCountry"
|
||||
label="کشور مبدا"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.quantity"
|
||||
label="تعداد"
|
||||
type="number"
|
||||
:rules="[rules.required, rules.positive]"
|
||||
required
|
||||
min="1"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.unitPrice)"
|
||||
label="قیمت واحد (ارزی)"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.required, rules.positiveMoney]"
|
||||
required
|
||||
@update:modelValue="onMoneyInput('unitPrice', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.unitPriceIRR)"
|
||||
label="قیمت واحد (ریال)"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.required, rules.positiveMoney]"
|
||||
required
|
||||
@update:modelValue="onMoneyInput('unitPriceIRR', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(totalPrice)"
|
||||
label="قیمت کل"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.weight"
|
||||
label="وزن (کیلوگرم)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:rules="[rules.positive]"
|
||||
min="0"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.volume"
|
||||
label="حجم (متر مکعب)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:rules="[rules.positive]"
|
||||
min="0"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.specifications"
|
||||
label="ویژگیها"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingItem ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>حذف آیتم</v-card-title>
|
||||
<v-card-text>
|
||||
آیا از حذف آیتم "{{ selectedItem?.name }}" اطمینان دارید؟
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const editingItem = ref(null)
|
||||
const selectedItem = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
productCode: '',
|
||||
brand: '',
|
||||
model: '',
|
||||
originCountry: '',
|
||||
quantity: '',
|
||||
unitPrice: '',
|
||||
unitPriceIRR: '',
|
||||
totalPrice: '',
|
||||
totalPriceIRR: '',
|
||||
weight: '',
|
||||
volume: '',
|
||||
specifications: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'نام کالا', key: 'name', sortable: false },
|
||||
{ title: 'برند', key: 'brand', sortable: false },
|
||||
{ title: 'مدل', key: 'model', sortable: false },
|
||||
{ title: 'کشور مبدا', key: 'originCountry', sortable: false },
|
||||
{ title: 'تعداد', key: 'quantity', sortable: false },
|
||||
{ title: 'قیمت واحد', key: 'unitPrice', sortable: false },
|
||||
{ title: 'قیمت کل', key: 'totalPrice', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||
positiveMoney: (value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers for money formatting/parse and LTR input
|
||||
const parseMoneyInput = (val) => {
|
||||
if (val === null || val === undefined) return 0
|
||||
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||
const num = Number(cleaned)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
}
|
||||
|
||||
const onMoneyInput = (field, value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
formData.value[field] = numeric
|
||||
}
|
||||
|
||||
const formatMoney = (value) => {
|
||||
const numericValue = Number(value) || 0
|
||||
return numericValue
|
||||
.toFixed(0)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// Computed
|
||||
const totalPrice = computed(() => {
|
||||
if (formData.value.quantity && formData.value.unitPrice) {
|
||||
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPrice)
|
||||
formData.value.totalPrice = total.toString()
|
||||
return formatNumber(total)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// Watch for unit price IRR and quantity changes
|
||||
watch([() => formData.value.quantity, () => formData.value.unitPriceIRR], () => {
|
||||
if (formData.value.quantity && formData.value.unitPriceIRR) {
|
||||
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPriceIRR)
|
||||
formData.value.totalPriceIRR = total.toString()
|
||||
}
|
||||
})
|
||||
|
||||
// Methods
|
||||
const editItem = (item) => {
|
||||
editingItem.value = item
|
||||
formData.value = { ...item }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteItem = (item) => {
|
||||
selectedItem.value = item
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const saveItem = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const url = editingItem.value
|
||||
? `/api/import-workflow/${props.workflowId}/items/${editingItem.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/items/create`
|
||||
|
||||
const method = editingItem.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingItem.value ? 'آیتم با موفقیت ویرایش شد' : 'آیتم با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
cancelEdit()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving item:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره آیتم خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/items/${selectedItem.value.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: 'آیتم با موفقیت حذف شد',
|
||||
icon: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting item:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در حذف آیتم خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
deleteLoading.value = false
|
||||
showDeleteDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingItem.value = null
|
||||
formData.value = {
|
||||
name: '',
|
||||
productCode: '',
|
||||
brand: '',
|
||||
model: '',
|
||||
originCountry: '',
|
||||
quantity: '',
|
||||
unitPrice: '',
|
||||
unitPriceIRR: '',
|
||||
totalPrice: '',
|
||||
totalPriceIRR: '',
|
||||
weight: '',
|
||||
volume: '',
|
||||
specifications: '',
|
||||
description: ''
|
||||
}
|
||||
showAddDialog.value = false
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
|
||||
const getCurrency = (item) => {
|
||||
// This should be based on the workflow's currency
|
||||
return 'USD'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-items {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
<template>
|
||||
<div class="import-workflow-payments">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>پرداختها</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن پرداخت
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="payments"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="پرداختی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeColor(item.type)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getTypeText(item.type) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.amount="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.amount) }}
|
||||
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.amountIRR="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.amountIRR) }}
|
||||
<small class="text-medium-emphasis">ریال</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.paymentDate="{ item }">
|
||||
{{ formatDate(item.paymentDate) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editPayment(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deletePayment(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingPayment ? 'ویرایش پرداخت' : 'افزودن پرداخت جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="savePayment">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.type"
|
||||
:items="paymentTypes"
|
||||
label="نوع پرداخت"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.status"
|
||||
:items="statusOptions"
|
||||
label="وضعیت"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.amount)"
|
||||
label="مبلغ"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.required, rules.positiveMoney]"
|
||||
required
|
||||
@update:modelValue="onMoneyInput('amount', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="formData.currency"
|
||||
:items="currencyOptions"
|
||||
label="واحد پول"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(formData.amountIRR)"
|
||||
label="مبلغ (ریال)"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney]"
|
||||
@update:modelValue="onMoneyInput('amountIRR', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.paymentDate"
|
||||
label="تاریخ پرداخت"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.referenceNumber"
|
||||
label="شماره مرجع"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.bankName"
|
||||
label="نام بانک"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.accountNumber"
|
||||
label="شماره حساب"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.recipientName"
|
||||
label="نام دریافت کننده"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.receiptNumber"
|
||||
label="شماره رسید"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingPayment ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>حذف پرداخت</v-card-title>
|
||||
<v-card-text>
|
||||
آیا از حذف این پرداخت اطمینان دارید؟
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
payments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const editingPayment = ref(null)
|
||||
const selectedPayment = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
type: '',
|
||||
amount: '',
|
||||
currency: 'USD',
|
||||
amountIRR: '',
|
||||
paymentDate: '',
|
||||
referenceNumber: '',
|
||||
bankName: '',
|
||||
accountNumber: '',
|
||||
recipientName: '',
|
||||
status: 'pending',
|
||||
description: '',
|
||||
receiptNumber: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'نوع پرداخت', key: 'type', sortable: false },
|
||||
{ title: 'مبلغ', key: 'amount', sortable: false },
|
||||
{ title: 'مبلغ (ریال)', key: 'amountIRR', sortable: false },
|
||||
{ title: 'تاریخ پرداخت', key: 'paymentDate', sortable: false },
|
||||
{ title: 'دریافت کننده', key: 'recipientName', sortable: false },
|
||||
{ title: 'وضعیت', key: 'status', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Options
|
||||
const paymentTypes = [
|
||||
{ title: 'پرداخت به تامین کننده', value: 'supplier' },
|
||||
{ title: 'پرداخت ترخیصکار', value: 'customs_broker' },
|
||||
{ title: 'پرداخت حمل و نقل', value: 'shipping' },
|
||||
{ title: 'پرداخت صرافی', value: 'exchange' },
|
||||
{ title: 'پرداخت بیمه', value: 'insurance' },
|
||||
{ title: 'پرداخت انبار موقت', value: 'temporary_storage' },
|
||||
{ title: 'سایر هزینهها', value: 'other' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'در انتظار پرداخت', value: 'pending' },
|
||||
{ title: 'پرداخت شده', value: 'paid' },
|
||||
{ title: 'تایید شده', value: 'confirmed' },
|
||||
{ title: 'لغو شده', value: 'cancelled' }
|
||||
]
|
||||
|
||||
const currencyOptions = [
|
||||
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||
positiveMoney: (value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers for money formatting/parse and LTR input
|
||||
const parseMoneyInput = (val) => {
|
||||
if (val === null || val === undefined) return 0
|
||||
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||
const num = Number(cleaned)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
}
|
||||
|
||||
const onMoneyInput = (field, value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
formData.value[field] = numeric
|
||||
}
|
||||
|
||||
const formatMoney = (value) => {
|
||||
const numericValue = Number(value) || 0
|
||||
return numericValue
|
||||
.toFixed(0)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// Methods
|
||||
const editPayment = (payment) => {
|
||||
editingPayment.value = payment
|
||||
formData.value = { ...payment }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deletePayment = (payment) => {
|
||||
selectedPayment.value = payment
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const savePayment = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const url = editingPayment.value
|
||||
? `/api/import-workflow/${props.workflowId}/payments/${editingPayment.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/payments/create`
|
||||
|
||||
const method = editingPayment.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingPayment.value ? 'پرداخت با موفقیت ویرایش شد' : 'پرداخت با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
cancelEdit()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving payment:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره پرداخت خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/payments/${selectedPayment.value.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: 'پرداخت با موفقیت حذف شد',
|
||||
icon: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting payment:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در حذف پرداخت خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
deleteLoading.value = false
|
||||
showDeleteDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingPayment.value = null
|
||||
formData.value = {
|
||||
type: '',
|
||||
amount: '',
|
||||
currency: 'USD',
|
||||
amountIRR: '',
|
||||
paymentDate: '',
|
||||
referenceNumber: '',
|
||||
bankName: '',
|
||||
accountNumber: '',
|
||||
recipientName: '',
|
||||
status: 'pending',
|
||||
description: '',
|
||||
receiptNumber: ''
|
||||
}
|
||||
showAddDialog.value = false
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
supplier: 'blue',
|
||||
customs_broker: 'orange',
|
||||
shipping: 'purple',
|
||||
exchange: 'teal',
|
||||
insurance: 'green',
|
||||
temporary_storage: 'brown',
|
||||
other: 'grey'
|
||||
}
|
||||
return colors[type] || 'grey'
|
||||
}
|
||||
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
supplier: 'تامین کننده',
|
||||
customs_broker: 'ترخیصکار',
|
||||
shipping: 'حمل و نقل',
|
||||
exchange: 'صرافی',
|
||||
insurance: 'بیمه',
|
||||
temporary_storage: 'انبار موقت',
|
||||
other: 'سایر'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
paid: 'blue',
|
||||
confirmed: 'green',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: 'در انتظار',
|
||||
paid: 'پرداخت شده',
|
||||
confirmed: 'تایید شده',
|
||||
cancelled: 'لغو شده'
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleDateString('fa-IR')
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
cancelEdit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-payments {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
<template>
|
||||
<div class="import-workflow-shipping">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>اطلاعات حمل و نقل</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن اطلاعات حمل
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="shipping"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="اطلاعات حمل و نقلی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeColor(item.type)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getTypeText(item.type) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editShipping(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteShipping(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingShipping ? 'ویرایش اطلاعات حمل' : 'افزودن اطلاعات حمل جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveShipping">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.type"
|
||||
:items="shippingTypes"
|
||||
label="نوع حمل"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.shippingCompany"
|
||||
label="شرکت حمل و نقل"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.containerNumber"
|
||||
label="شماره کانتینر"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.billOfLading"
|
||||
label="شماره بارنامه"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<h-date-picker
|
||||
v-model="formData.shippingDate"
|
||||
label="تاریخ حمل"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<h-date-picker
|
||||
v-model="formData.arrivalDate"
|
||||
label="تاریخ رسیدن"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<h-date-picker
|
||||
v-model="formData.unloadingDate"
|
||||
label="تاریخ تخلیه"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.originPort"
|
||||
label="بندر مبدا"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.destinationPort"
|
||||
label="بندر مقصد"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.vesselName"
|
||||
label="نام کشتی"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.voyageNumber"
|
||||
label="شماره سفر"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saveLoading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingShipping ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
shipping: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const editingShipping = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
type: '',
|
||||
containerNumber: '',
|
||||
billOfLading: '',
|
||||
shippingDate: '',
|
||||
arrivalDate: '',
|
||||
unloadingDate: '',
|
||||
shippingCompany: '',
|
||||
originPort: '',
|
||||
destinationPort: '',
|
||||
vesselName: '',
|
||||
voyageNumber: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'نوع حمل', key: 'type', sortable: false },
|
||||
{ title: 'شرکت حمل', key: 'shippingCompany', sortable: false },
|
||||
{ title: 'شماره کانتینر', key: 'containerNumber', sortable: false },
|
||||
{ title: 'بندر مبدا', key: 'originPort', sortable: false },
|
||||
{ title: 'بندر مقصد', key: 'destinationPort', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Shipping types
|
||||
const shippingTypes = [
|
||||
{ title: 'دریایی', value: 'sea' },
|
||||
{ title: 'هوایی', value: 'air' },
|
||||
{ title: 'زمینی', value: 'land' },
|
||||
{ title: 'ترکیبی', value: 'multimodal' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
maxLength: (value) => !value || value.length <= 500 || 'حداکثر 500 کاراکتر مجاز است',
|
||||
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید'
|
||||
}
|
||||
|
||||
// Methods
|
||||
const editShipping = (shipping) => {
|
||||
editingShipping.value = shipping
|
||||
formData.value = { ...shipping }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteShipping = async (shipping) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف اطلاعات حمل',
|
||||
text: 'آیا از حذف این اطلاعات حمل و نقل اطمینان دارید؟',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'لغو'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/shipping/${shipping.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire('موفق', 'اطلاعات حمل با موفقیت حذف شد', 'success')
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('خطا', 'در حذف اطلاعات حمل خطایی رخ داد', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveShipping = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const url = editingShipping.value
|
||||
? `/api/import-workflow/${props.workflowId}/shipping/${editingShipping.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/shipping/create`
|
||||
|
||||
const method = editingShipping.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingShipping.value ? 'اطلاعات حمل با موفقیت ویرایش شد' : 'اطلاعات حمل با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
closeDialog()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving shipping:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره اطلاعات حمل خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
editingShipping.value = null
|
||||
formData.value = {
|
||||
type: '',
|
||||
containerNumber: '',
|
||||
billOfLading: '',
|
||||
shippingDate: '',
|
||||
arrivalDate: '',
|
||||
unloadingDate: '',
|
||||
shippingCompany: '',
|
||||
originPort: '',
|
||||
destinationPort: '',
|
||||
vesselName: '',
|
||||
voyageNumber: '',
|
||||
description: ''
|
||||
}
|
||||
showAddDialog.value = false
|
||||
if (form.value) {
|
||||
form.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
sea: 'blue',
|
||||
air: 'purple',
|
||||
land: 'green',
|
||||
multimodal: 'orange'
|
||||
}
|
||||
return colors[type] || 'grey'
|
||||
}
|
||||
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
sea: 'دریایی',
|
||||
air: 'هوایی',
|
||||
land: 'زمینی',
|
||||
multimodal: 'ترکیبی'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-shipping {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
<template>
|
||||
<div class="import-workflow-stages">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>مراحل واردات</h3>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
افزودن مرحله
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-timeline direction="horizontal" class="mb-4">
|
||||
<v-timeline-item
|
||||
v-for="stage in stages"
|
||||
:key="stage.id"
|
||||
:dot-color="getStatusColor(stage.status)"
|
||||
size="small"
|
||||
>
|
||||
<template v-slot:icon>
|
||||
<v-icon size="small">{{ getStageIcon(stage.stage) }}</v-icon>
|
||||
</template>
|
||||
<v-card class="elevation-2">
|
||||
<v-card-title class="text-h6">
|
||||
{{ getStageText(stage.stage) }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-chip
|
||||
:color="getStatusColor(stage.status)"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ getStatusText(stage.status) }}
|
||||
</v-chip>
|
||||
<div v-if="stage.startDate">
|
||||
<small>تاریخ شروع: {{ formatDate(stage.startDate) }}</small>
|
||||
</div>
|
||||
<div v-if="stage.endDate">
|
||||
<small>تاریخ پایان: {{ formatDate(stage.endDate) }}</small>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editStage(stage)"
|
||||
>
|
||||
ویرایش
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="stages"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
no-data-text="مرحلهای ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.stage="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="ml-2">{{ getStageIcon(item.stage) }}</v-icon>
|
||||
{{ getStageText(item.stage) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editStage(item)"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteStage(item)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<v-dialog v-model="showAddDialog" max-width="600" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
{{ editingStage ? 'ویرایش مرحله' : 'افزودن مرحله جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveStage">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.stage"
|
||||
:items="stageTypes"
|
||||
label="نوع مرحله"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.status"
|
||||
:items="statusOptions"
|
||||
label="وضعیت"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.startDate"
|
||||
label="تاریخ شروع"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.endDate"
|
||||
label="تاریخ پایان"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="formData.assignedTo"
|
||||
label="مسئول"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.notes"
|
||||
label="یادداشتها"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeDialog">لغو</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="saveLoading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
{{ editingStage ? 'ویرایش' : 'افزودن' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
workflowId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
stages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// Data
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const editingStage = ref(null)
|
||||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
stage: '',
|
||||
status: 'pending',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
assignedTo: '',
|
||||
description: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'مرحله', key: 'stage', sortable: false },
|
||||
{ title: 'وضعیت', key: 'status', sortable: false },
|
||||
{ title: 'تاریخ شروع', key: 'startDate', sortable: false },
|
||||
{ title: 'تاریخ پایان', key: 'endDate', sortable: false },
|
||||
{ title: 'مسئول', key: 'assignedTo', sortable: false },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||
]
|
||||
|
||||
// Stage types
|
||||
const stageTypes = [
|
||||
{ title: 'صدور پیشفاکتور', value: 'proforma_issue' },
|
||||
{ title: 'تایید سفارش', value: 'order_confirmation' },
|
||||
{ title: 'پرداخت', value: 'payment' },
|
||||
{ title: 'آمادهسازی کالا', value: 'goods_preparation' },
|
||||
{ title: 'حمل و نقل', value: 'shipping' },
|
||||
{ title: 'رسیدن کالا', value: 'arrival' },
|
||||
{ title: 'ترخیص گمرکی', value: 'customs_clearance' },
|
||||
{ title: 'انتقال به انبار', value: 'warehouse_transfer' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'در انتظار', value: 'pending' },
|
||||
{ title: 'در حال انجام', value: 'in_progress' },
|
||||
{ title: 'تکمیل شده', value: 'completed' },
|
||||
{ title: 'لغو شده', value: 'cancelled' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است'
|
||||
}
|
||||
|
||||
// Methods
|
||||
const editStage = (stage) => {
|
||||
editingStage.value = stage
|
||||
formData.value = { ...stage }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteStage = async (stage) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف مرحله',
|
||||
text: `آیا از حذف مرحله "${getStageText(stage.stage)}" اطمینان دارید؟`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'لغو'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/stages/${stage.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire('موفق', 'مرحله با موفقیت حذف شد', 'success')
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('خطا', 'در حذف مرحله خطایی رخ داد', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveStage = async () => {
|
||||
if (!valid.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const url = editingStage.value
|
||||
? `/api/import-workflow/${props.workflowId}/stages/${editingStage.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/stages/create`
|
||||
|
||||
const method = editingStage.value ? 'PUT' : 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: editingStage.value ? 'مرحله با موفقیت ویرایش شد' : 'مرحله با موفقیت افزوده شد',
|
||||
icon: 'success'
|
||||
})
|
||||
cancelEdit()
|
||||
emit('updated')
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving stage:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره مرحله خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
editingStage.value = null
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getStageIcon = (stage) => {
|
||||
const icons = {
|
||||
proforma_issue: 'mdi-file-document-outline',
|
||||
order_confirmation: 'mdi-check-circle-outline',
|
||||
payment: 'mdi-credit-card-outline',
|
||||
goods_preparation: 'mdi-package-variant',
|
||||
shipping: 'mdi-truck-delivery-outline',
|
||||
arrival: 'mdi-map-marker-check',
|
||||
customs_clearance: 'mdi-gavel',
|
||||
warehouse_transfer: 'mdi-warehouse'
|
||||
}
|
||||
return icons[stage] || 'mdi-circle-outline'
|
||||
}
|
||||
|
||||
const getStageText = (stage) => {
|
||||
const texts = {
|
||||
proforma_issue: 'صدور پیشفاکتور',
|
||||
order_confirmation: 'تایید سفارش',
|
||||
payment: 'پرداخت',
|
||||
goods_preparation: 'آمادهسازی کالا',
|
||||
shipping: 'حمل و نقل',
|
||||
arrival: 'رسیدن کالا',
|
||||
customs_clearance: 'ترخیص گمرکی',
|
||||
warehouse_transfer: 'انتقال به انبار'
|
||||
}
|
||||
return texts[stage] || stage
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
in_progress: 'blue',
|
||||
completed: 'green',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
pending: 'در انتظار',
|
||||
in_progress: 'در حال انجام',
|
||||
completed: 'تکمیل شده',
|
||||
cancelled: 'لغو شده'
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleDateString('fa-IR')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-stages {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -1061,20 +1061,20 @@ const router = createRouter({
|
|||
import('../views/acc/inquiry/panel.vue'),
|
||||
},
|
||||
{
|
||||
path: 'import-workflow/list',
|
||||
path: 'plugins/import-workflow/list',
|
||||
name: 'import_workflow_list',
|
||||
component: () =>
|
||||
import('../views/ImportWorkflow/ImportWorkflowList.vue'),
|
||||
import('../views/acc/plugins/import-workflow/list.vue'),
|
||||
meta: {
|
||||
'title': 'مدیریت واردات کالا',
|
||||
'login': true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'import-workflow/:id',
|
||||
path: 'plugins/import-workflow/:id',
|
||||
name: 'import_workflow_detail',
|
||||
component: () =>
|
||||
import('../views/ImportWorkflow/ImportWorkflowDetail.vue'),
|
||||
import('../views/acc/plugins/import-workflow/view.vue'),
|
||||
meta: {
|
||||
'title': 'جزئیات پرونده واردات',
|
||||
'login': true,
|
||||
|
|
|
@ -216,7 +216,7 @@ export default {
|
|||
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||
{ path: '/acc/plugins/custominvoice/templates', key: 'I', label: 'قالبهای فاکتور', ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('custominvoice') },
|
||||
{ path: '/acc/import-workflow/list', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.importWorkflow },
|
||||
{ path: '/acc/plugins/import-workflow', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.importWorkflow },
|
||||
];
|
||||
},
|
||||
restorePermissions(shortcuts) {
|
||||
|
@ -807,20 +807,21 @@ export default {
|
|||
</v-list-item>
|
||||
</v-list-group>
|
||||
<v-list-subheader color="primary">{{ $t('drawer.services') }}</v-list-subheader>
|
||||
<v-list-group v-show="permissions.importWorkflow">
|
||||
<v-list-group v-show="isPluginActive('import-workflow') && permissions.importWorkflow">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item class="text-dark" v-bind="props" title="مدیریت واردات کالا">
|
||||
<template v-slot:prepend><v-icon icon="mdi-import" color="primary"></v-icon></template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-list-item v-if="permissions.importWorkflow" to="/acc/import-workflow/list">
|
||||
<v-list-item v-if="permissions.importWorkflow" to="/acc/plugins/import-workflow/list">
|
||||
<v-list-item-title>
|
||||
لیست پروندههای واردات
|
||||
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/import-workflow/list') }}</span>
|
||||
</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-tooltip text="پرونده واردات جدید" location="end">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" @click="$router.push('/acc/import-workflow/list')" />
|
||||
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" @click="$router.push('/acc/plugins/import-workflow/list')" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
<v-btn color="primary" size="small" @click="dialog = true" :loading="loading" prepend-icon="mdi-bank">
|
||||
{{ $t('dialog.banks_accounts') }}
|
||||
</v-btn>
|
||||
<v-btn color="warning" size="small" @click="editPersonDialog = true" :loading="loading" prepend-icon="mdi-account-edit" class="ml-2">
|
||||
ویرایش شخص
|
||||
</v-btn>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="" color="red">
|
||||
|
@ -91,6 +94,94 @@
|
|||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- دیالوگ ویرایش شخص -->
|
||||
<v-dialog v-model="editPersonDialog" max-width="600" persistent>
|
||||
<v-card>
|
||||
<v-toolbar color="primary-dark" dense flat>
|
||||
<v-toolbar-title class="text-white">ویرایش شخص</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn icon @click="editPersonDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-4">
|
||||
<v-form ref="editPersonForm" v-model="editPersonFormValid">
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.nikename"
|
||||
label="نام مستعار"
|
||||
dense
|
||||
required
|
||||
:rules="[v => !!v || 'نام مستعار الزامی است']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.name"
|
||||
label="نام کامل"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.mobile"
|
||||
label="موبایل"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.tel"
|
||||
label="تلفن"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.address"
|
||||
label="آدرس"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editPersonData.des"
|
||||
label="توضیحات"
|
||||
dense
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-2" />
|
||||
<v-card-subtitle class="px-0 text-primary">
|
||||
تأیید دو مرحلهای
|
||||
</v-card-subtitle>
|
||||
<v-switch
|
||||
v-model="editPersonData.requireTwoStep"
|
||||
label="فاکتورها و حوالههای این شخص نیاز به تأیید دو مرحلهای دارند"
|
||||
color="warning"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
/>
|
||||
<v-alert type="info" variant="tonal" dense class="mt-2">
|
||||
اگر این گزینه فعال باشد، تمام فاکتورها، حوالههای انبار و اسناد مالی مرتبط با این شخص نیاز به تأیید دو مرحلهای خواهند داشت.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="secondary" text @click="editPersonDialog = false">
|
||||
انصراف
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="savePersonChanges" :loading="saveLoading" :disabled="!editPersonFormValid">
|
||||
ذخیره
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- محتوای اصلی -->
|
||||
<v-container fluid class="pa-4">
|
||||
<v-row dense>
|
||||
|
@ -151,6 +242,21 @@
|
|||
selectedPerson.address || '-' }}</span></div>
|
||||
<div class="text-subtitle-2">{{ $t('pages.person.description') }}: <span class="text-primary">{{
|
||||
selectedPerson.des || '-' }}</span></div>
|
||||
<!-- <v-divider class="my-3" />
|
||||
<div class="text-subtitle-2 d-flex align-center justify-space-between">
|
||||
<span>تأیید دو مرحلهای:</span>
|
||||
<v-switch
|
||||
v-model="selectedPerson.requireTwoStep"
|
||||
color="warning"
|
||||
hide-details
|
||||
density="compact"
|
||||
@change="updateTwoStepApproval"
|
||||
:loading="twoStepUpdateLoading"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-caption text-grey mt-1">
|
||||
{{ selectedPerson.requireTwoStep ? 'فعال - فاکتورها و حوالهها نیاز به تأیید دارند' : 'غیرفعال - طبق قوانین عادی' }}
|
||||
</div> -->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -179,6 +285,13 @@
|
|||
$filters.formatNumber(selectedPerson.bd) || '-' }}</span></div>
|
||||
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_balance') }}: <span class="text-primary">{{
|
||||
$filters.formatNumber(selectedPerson.balance) || '-' }}</span></div>
|
||||
<v-divider class="my-2" />
|
||||
<div class="text-subtitle-2">
|
||||
تأیید دو مرحلهای:
|
||||
<v-chip :color="selectedPerson.requireTwoStep ? 'warning' : 'success'" size="small" class="ml-2">
|
||||
{{ selectedPerson.requireTwoStep ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -259,10 +372,22 @@ export default {
|
|||
searchValue: '',
|
||||
listPersons: [],
|
||||
itemsSelected: [],
|
||||
selectedPerson: { accounts: [], balance: 0, bs: 0, bd: 0 },
|
||||
selectedPerson: { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false },
|
||||
items: [],
|
||||
loading: ref(false),
|
||||
dialog: false,
|
||||
editPersonDialog: false,
|
||||
editPersonFormValid: false,
|
||||
saveLoading: false,
|
||||
editPersonData: {
|
||||
nikename: '',
|
||||
name: '',
|
||||
mobile: '',
|
||||
tel: '',
|
||||
address: '',
|
||||
des: '',
|
||||
requireTwoStep: false
|
||||
},
|
||||
debounceTimeout: null, // برای مدیریت debounce
|
||||
headers: [
|
||||
{ title: this.$t('dialog.operation'), key: "operation", align: "center", sortable: false },
|
||||
|
@ -338,17 +463,50 @@ export default {
|
|||
try {
|
||||
const personResponse = await axios.post('/api/person/info/' + id);
|
||||
this.selectedPerson = personResponse.data;
|
||||
// پر کردن فرم ویرایش با اطلاعات فعلی شخص
|
||||
this.editPersonData = {
|
||||
nikename: this.selectedPerson.nikename || '',
|
||||
name: this.selectedPerson.name || '',
|
||||
mobile: this.selectedPerson.mobile || '',
|
||||
tel: this.selectedPerson.tel || '',
|
||||
address: this.selectedPerson.address || '',
|
||||
des: this.selectedPerson.des || '',
|
||||
requireTwoStep: this.selectedPerson.requireTwoStep || false
|
||||
};
|
||||
|
||||
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'person', id });
|
||||
this.items = rowsResponse.data;
|
||||
} catch (error) {
|
||||
console.error('Load person error:', error);
|
||||
this.selectedPerson = { accounts: [], balance: 0, bs: 0, bd: 0 };
|
||||
this.selectedPerson = { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false };
|
||||
this.items = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async savePersonChanges() {
|
||||
if (!this.selectedPerson || !this.selectedPerson.code) {
|
||||
this.snackbar = { show: true, text: 'شخص انتخاب نشده است', color: 'error' };
|
||||
return;
|
||||
}
|
||||
this.saveLoading = true;
|
||||
try {
|
||||
const response = await axios.post('/api/person/mod/' + this.selectedPerson.code, this.editPersonData);
|
||||
if (response.data.Success) {
|
||||
this.snackbar = { show: true, text: 'اطلاعات شخص با موفقیت بروزرسانی شد', color: 'success' };
|
||||
this.editPersonDialog = false;
|
||||
// بروزرسانی اطلاعات شخص
|
||||
await this.loadPerson(this.selectedPerson.code);
|
||||
} else {
|
||||
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save person error:', error);
|
||||
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
|
||||
} finally {
|
||||
this.saveLoading = false;
|
||||
}
|
||||
},
|
||||
async excellOutput(allItems = true) {
|
||||
if (!allItems && this.itemsSelected.length === 0) {
|
||||
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
|
||||
|
@ -429,6 +587,16 @@ export default {
|
|||
return labels[type] || type;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
snackbar: {
|
||||
get() {
|
||||
return this.$store.state.snackbar;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('setSnackbar', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -65,6 +65,23 @@
|
|||
<v-text-field v-model="person.des" :label="$t('pages.person.description')" dense
|
||||
prepend-inner-icon="mdi-text" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-2" />
|
||||
<v-card-subtitle class="px-0 text-primary mt-2">
|
||||
تأیید دو مرحلهای
|
||||
</v-card-subtitle>
|
||||
<v-switch
|
||||
v-model="person.requireTwoStep"
|
||||
label="فاکتورها و حوالههای این شخص نیاز به تأیید دو مرحلهای دارند"
|
||||
color="warning"
|
||||
inset
|
||||
hide-details
|
||||
class="mt-2"
|
||||
/>
|
||||
<v-alert type="info" variant="tonal" dense class="mt-2">
|
||||
اگر این گزینه فعال باشد، تمام فاکتورها، حوالههای انبار و اسناد مالی مرتبط با این شخص نیاز به تأیید دو مرحلهای خواهند داشت.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -287,7 +304,8 @@ export default {
|
|||
types: [],
|
||||
accounts: [],
|
||||
prelabel: ref(null),
|
||||
speedAccess: false
|
||||
speedAccess: false,
|
||||
requireTwoStep: false
|
||||
},
|
||||
snackbar: {
|
||||
show: false,
|
||||
|
@ -404,6 +422,8 @@ export default {
|
|||
if (canSubmit) {
|
||||
this.loading = true;
|
||||
try {
|
||||
console.log('Saving person data:', this.person);
|
||||
console.log('requireTwoStep value:', this.person.requireTwoStep);
|
||||
const response = await axios.post('/api/person/mod/' + this.person.code, this.person);
|
||||
this.loading = false;
|
||||
if (response.data && response.data.result === 2) {
|
||||
|
|
|
@ -35,6 +35,13 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-tooltip text="تایید پرداختهای انتخابی" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon color="success" @click="approveSelectedPayments">
|
||||
<v-icon>mdi-check-decagram</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon color="success">
|
||||
|
@ -434,6 +441,28 @@ const updateSelectedSum = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const approveSelectedPayments = async () => {
|
||||
if (selectedItems.value.length === 0) {
|
||||
Swal.fire({ text: 'هیچ آیتمی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
|
||||
return;
|
||||
}
|
||||
const res = await Swal.fire({ title: 'تایید پرداختها', text: 'پرداختهای انتخابی تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' });
|
||||
if (!res.isConfirmed) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
for (const it of selectedItems.value) {
|
||||
await axios.post(`/api/sell/payment/approve/${it.code}`);
|
||||
}
|
||||
Swal.fire({ text: 'پرداختها تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
|
||||
selectedItems.value = [];
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
Swal.fire({ text: 'خطا در تایید پرداختها', icon: 'error', confirmButtonText: 'قبول' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const print = async (allItems = true) => {
|
||||
if (!allItems && selectedItems.value.length === 0) {
|
||||
Swal.fire({
|
||||
|
|
681
webUI/src/views/acc/plugins/import-workflow/list.vue
Normal file
681
webUI/src/views/acc/plugins/import-workflow/list.vue
Normal file
|
@ -0,0 +1,681 @@
|
|||
<template>
|
||||
<div class="import-workflow-list">
|
||||
<v-container fluid>
|
||||
<!-- Stats Cards -->
|
||||
<v-row>
|
||||
<v-col cols="6" sm="6" md="3">
|
||||
<div class="stats-card total-card">
|
||||
<div class="stats-icon">
|
||||
<v-icon size="24" color="white" class="d-sm-none">mdi-import</v-icon>
|
||||
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-import</v-icon>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-number">
|
||||
<v-progress-circular
|
||||
v-if="statsLoading"
|
||||
indeterminate
|
||||
size="20"
|
||||
color="white"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
{{ stats.totalWorkflows || 0 }}
|
||||
</div>
|
||||
<div class="stats-label">کل پروندهها</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" md="3">
|
||||
<div :class="getCardClasses('draft')" @click="filterByStatus('draft')" role="button" tabindex="0">
|
||||
<div class="stats-icon">
|
||||
<v-icon size="24" color="white" class="d-sm-none">mdi-file-document-outline</v-icon>
|
||||
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-file-document-outline</v-icon>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-number">
|
||||
<v-progress-circular
|
||||
v-if="statsLoading"
|
||||
indeterminate
|
||||
size="20"
|
||||
color="white"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
{{ stats.draftWorkflows || 0 }}
|
||||
</div>
|
||||
<div class="stats-label">پیشنویس</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" md="3">
|
||||
<div :class="getCardClasses('processing')" @click="filterByStatus('processing')" role="button" tabindex="0">
|
||||
<div class="stats-icon">
|
||||
<v-icon size="24" color="white" class="d-sm-none">mdi-progress-clock</v-icon>
|
||||
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-progress-clock</v-icon>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-number">
|
||||
<v-progress-circular
|
||||
v-if="statsLoading"
|
||||
indeterminate
|
||||
size="20"
|
||||
color="white"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
{{ stats.processingWorkflows || 0 }}
|
||||
</div>
|
||||
<div class="stats-label">در حال پردازش</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" md="3">
|
||||
<div :class="getCardClasses('completed')" @click="filterByStatus('completed')" role="button" tabindex="0">
|
||||
<div class="stats-icon">
|
||||
<v-icon size="24" color="white" class="d-sm-none">mdi-check-circle</v-icon>
|
||||
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-check-circle</v-icon>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stats-number">
|
||||
<v-progress-circular
|
||||
v-if="statsLoading"
|
||||
indeterminate
|
||||
size="20"
|
||||
color="white"
|
||||
class="me-2"
|
||||
></v-progress-circular>
|
||||
{{ stats.completedWorkflows || 0 }}
|
||||
</div>
|
||||
<div class="stats-label">تکمیل شده</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Data Table -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="workflows"
|
||||
:loading="loading"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
hover
|
||||
>
|
||||
<template v-slot:top>
|
||||
<!-- موبایل -->
|
||||
<div class="d-block d-md-none pa-4">
|
||||
<div class="d-flex gap-2 flex-column mb-3">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showCreateDialog = true"
|
||||
size="small"
|
||||
block
|
||||
>
|
||||
پرونده واردات جدید
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="filters.search"
|
||||
label="جستجو"
|
||||
prepend-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="mb-3"
|
||||
@update:model-value="loadWorkflows"
|
||||
/>
|
||||
<v-select
|
||||
v-model="filters.status"
|
||||
label="وضعیت"
|
||||
:items="statusOptions"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="mb-3"
|
||||
@update:model-value="loadWorkflows"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- دسکتاپ -->
|
||||
<div class="d-none d-md-block">
|
||||
<v-toolbar flat style="height: 70px !important; padding: 10px !important;">
|
||||
<v-text-field
|
||||
v-model="filters.search"
|
||||
label="جستجو"
|
||||
prepend-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 250px;"
|
||||
@update:model-value="loadWorkflows"
|
||||
class="ml-2"
|
||||
/>
|
||||
<v-select
|
||||
v-model="filters.status"
|
||||
label="وضعیت"
|
||||
:items="statusOptions"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
@update:model-value="loadWorkflows"
|
||||
class="ml-2"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
پرونده واردات جدید
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.totalAmount="{ item }">
|
||||
<div class="text-left">
|
||||
{{ formatNumber(item.totalAmount) }}
|
||||
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.dateSubmit="{ item }">
|
||||
{{ formatDate(item.dateSubmit) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-menu"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
></v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="viewWorkflow(item)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="info">mdi-eye</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>مشاهده پرونده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="editWorkflow(item)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="warning">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ویرایش پرونده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteWorkflow(item)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>حذف پرونده</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<ImportWorkflowCreateDialog
|
||||
v-model="showCreateDialog"
|
||||
@created="onWorkflowCreated"
|
||||
/>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>حذف پرونده واردات</v-card-title>
|
||||
<v-card-text>
|
||||
آیا از حذف پرونده واردات "{{ selectedWorkflow?.title }}" اطمینان دارید؟
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar for notifications -->
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
location="bottom"
|
||||
class="rounded-lg"
|
||||
elevation="2"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :color="snackbarColor" class="me-2">
|
||||
{{ snackbarColor === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||
</v-icon>
|
||||
{{ snackbarText }}
|
||||
</div>
|
||||
<template v-slot:actions>
|
||||
<v-btn icon variant="text" @click="showSnackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import ImportWorkflowCreateDialog from '../../../../components/plugins/import-workflow/ImportWorkflowCreateDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Data
|
||||
const workflows = ref([])
|
||||
const loading = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const showCreateDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const selectedWorkflow = ref(null)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
// Stats
|
||||
const stats = ref({
|
||||
totalWorkflows: 0,
|
||||
draftWorkflows: 0,
|
||||
processingWorkflows: 0,
|
||||
completedWorkflows: 0
|
||||
})
|
||||
|
||||
// Notifications
|
||||
const showSnackbar = ref(false)
|
||||
const snackbarText = ref('')
|
||||
const snackbarColor = ref('success')
|
||||
|
||||
const showNotification = (text, color = 'success') => {
|
||||
snackbarText.value = text
|
||||
snackbarColor.value = color
|
||||
showSnackbar.value = true
|
||||
}
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
status: '',
|
||||
search: ''
|
||||
})
|
||||
|
||||
// Pagination
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// Headers
|
||||
const headers = [
|
||||
{ title: 'عملیات', key: 'actions', sortable: false },
|
||||
{ title: 'کد', key: 'code', sortable: true },
|
||||
{ title: 'عنوان', key: 'title', sortable: true },
|
||||
{ title: 'تامین کننده', key: 'supplierName', sortable: true },
|
||||
{ title: 'مبلغ کل', key: 'totalAmount', sortable: true },
|
||||
{ title: 'وضعیت', key: 'status', sortable: true },
|
||||
{ title: 'تاریخ ثبت', key: 'dateSubmit', sortable: true },
|
||||
{ title: 'ثبت کننده', key: 'submitter', sortable: true }
|
||||
]
|
||||
|
||||
// Status options
|
||||
const statusOptions = [
|
||||
{ title: 'پیشنویس', value: 'draft' },
|
||||
{ title: 'در حال پردازش', value: 'processing' },
|
||||
{ title: 'ارسال شده', value: 'shipped' },
|
||||
{ title: 'رسیده', value: 'arrived' },
|
||||
{ title: 'ترخیص شده', value: 'cleared' },
|
||||
{ title: 'تکمیل شده', value: 'completed' },
|
||||
{ title: 'لغو شده', value: 'cancelled' }
|
||||
]
|
||||
|
||||
// Methods
|
||||
const loadWorkflows = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.value.page,
|
||||
limit: pagination.value.limit
|
||||
}
|
||||
|
||||
if (filters.value.status) {
|
||||
params.status = filters.value.status
|
||||
}
|
||||
|
||||
if (filters.value.search) {
|
||||
params.search = filters.value.search
|
||||
}
|
||||
|
||||
const response = await axios.get('/api/import-workflow/list', { params })
|
||||
|
||||
if (response.data.Success) {
|
||||
workflows.value = response.data.Result.data
|
||||
pagination.value.total = response.data.Result.total
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading workflows:', error)
|
||||
showNotification('در بارگذاری لیست پروندههای واردات خطایی رخ داد', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
statsLoading.value = true
|
||||
const response = await axios.get('/api/import-workflow/stats')
|
||||
if (response.data.Success) {
|
||||
stats.value = response.data.Result
|
||||
} else {
|
||||
// Calculate stats from current data if API not available
|
||||
const totalWorkflows = workflows.value.length
|
||||
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
|
||||
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
|
||||
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
|
||||
|
||||
stats.value = {
|
||||
totalWorkflows,
|
||||
draftWorkflows,
|
||||
processingWorkflows,
|
||||
completedWorkflows
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری آمار:', error)
|
||||
// Calculate stats from current data
|
||||
const totalWorkflows = workflows.value.length
|
||||
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
|
||||
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
|
||||
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
|
||||
|
||||
stats.value = {
|
||||
totalWorkflows,
|
||||
draftWorkflows,
|
||||
processingWorkflows,
|
||||
completedWorkflows
|
||||
}
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updatePagination = (options) => {
|
||||
pagination.value.page = options.page
|
||||
pagination.value.limit = options.itemsPerPage
|
||||
loadWorkflows()
|
||||
}
|
||||
|
||||
const filterByStatus = (status) => {
|
||||
filters.value.status = filters.value.status === status ? '' : status
|
||||
loadWorkflows()
|
||||
}
|
||||
|
||||
const viewWorkflow = (workflow) => {
|
||||
router.push(`/acc/plugins/import-workflow/${workflow.id}`)
|
||||
}
|
||||
|
||||
const editWorkflow = (workflow) => {
|
||||
router.push(`/acc/plugins/import-workflow/${workflow.id}`)
|
||||
}
|
||||
|
||||
const deleteWorkflow = (workflow) => {
|
||||
selectedWorkflow.value = workflow
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
const response = await axios.delete(`/api/import-workflow/${selectedWorkflow.value.id}/delete`)
|
||||
|
||||
if (response.data.Success) {
|
||||
showNotification('پرونده واردات با موفقیت حذف شد')
|
||||
loadWorkflows()
|
||||
loadStats()
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting workflow:', error)
|
||||
showNotification('در حذف پرونده واردات خطایی رخ داد', 'error')
|
||||
} finally {
|
||||
deleteLoading.value = false
|
||||
showDeleteDialog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onWorkflowCreated = () => {
|
||||
showCreateDialog.value = false
|
||||
loadWorkflows()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
draft: 'grey',
|
||||
processing: 'blue',
|
||||
shipped: 'orange',
|
||||
arrived: 'purple',
|
||||
cleared: 'teal',
|
||||
completed: 'green',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
draft: 'پیشنویس',
|
||||
processing: 'در حال پردازش',
|
||||
shipped: 'ارسال شده',
|
||||
arrived: 'رسیده',
|
||||
cleared: 'ترخیص شده',
|
||||
completed: 'تکمیل شده',
|
||||
cancelled: 'لغو شده'
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
const getCardClasses = (status) => {
|
||||
const baseClasses = 'stats-card'
|
||||
const statusClasses = {
|
||||
'draft': 'draft-card',
|
||||
'processing': 'processing-card',
|
||||
'completed': 'completed-card'
|
||||
}
|
||||
|
||||
const classes = [baseClasses, statusClasses[status]]
|
||||
|
||||
if (filters.value.status === status) {
|
||||
classes.push('active-filter')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleDateString('fa-IR')
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadWorkflows()
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-list {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-header th) {
|
||||
background-color: #f5f5f5 !important;
|
||||
font-weight: bold !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table td) {
|
||||
padding: 12px 16px !important;
|
||||
border-bottom: 1px solid #e0e0e0 !important;
|
||||
}
|
||||
|
||||
:deep(.v-data-table__wrapper table tr:hover) {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.v-chip) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
color: white;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.stats-card:focus {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.stats-card.active-filter {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.stats-card.active-filter::before {
|
||||
background: linear-gradient(45deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%);
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||
border-radius: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-8px) scale(1.01);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Card Variants - Professional Colors */
|
||||
.total-card {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||
}
|
||||
|
||||
.draft-card {
|
||||
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
|
||||
}
|
||||
|
||||
.processing-card {
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
}
|
||||
|
||||
.completed-card {
|
||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-card {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
820
webUI/src/views/acc/plugins/import-workflow/view.vue
Normal file
820
webUI/src/views/acc/plugins/import-workflow/view.vue
Normal file
|
@ -0,0 +1,820 @@
|
|||
<template>
|
||||
<div class="import-workflow-detail">
|
||||
<v-container fluid>
|
||||
<!-- Header -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
icon="mdi-arrow-right"
|
||||
variant="text"
|
||||
@click="$router.back()"
|
||||
class="ml-2"
|
||||
></v-btn>
|
||||
<v-icon class="ml-2" color="primary">mdi-import</v-icon>
|
||||
<span>{{ workflow?.title || 'جزئیات پرونده واردات' }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-chip
|
||||
:color="getStatusColor(workflow?.status)"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ getStatusText(workflow?.status) }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-pencil"
|
||||
@click="editMode = !editMode"
|
||||
>
|
||||
{{ editMode ? 'لغو ویرایش' : 'ویرایش' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mr-2"
|
||||
color="success"
|
||||
prepend-icon="mdi-warehouse"
|
||||
@click="openCreateTicketDialog"
|
||||
>
|
||||
ایجاد حواله ورود از پرونده
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="loading">
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-else-if="workflow">
|
||||
<!-- Basic Info -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title>اطلاعات کلی</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form v-if="editMode" ref="form" v-model="valid" validate-on="input">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editData.title"
|
||||
label="عنوان پرونده"
|
||||
:rules="[rules.required, rules.minLength]"
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editData.supplierName"
|
||||
label="نام تامین کننده"
|
||||
:rules="[rules.required, rules.minLength]"
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editData.supplierCountry"
|
||||
label="کشور تامین کننده"
|
||||
:rules="[rules.maxLength]"
|
||||
counter="50"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editData.supplierPhone"
|
||||
label="تلفن تامین کننده"
|
||||
:rules="[rules.phone]"
|
||||
counter="20"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="editData.supplierEmail"
|
||||
label="ایمیل تامین کننده"
|
||||
:rules="[rules.email]"
|
||||
counter="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="editData.supplierAddress"
|
||||
label="آدرس تامین کننده"
|
||||
rows="2"
|
||||
:rules="[rules.maxLength]"
|
||||
counter="500"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(editData.totalAmount)"
|
||||
label="مبلغ کل (ارزی)"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.positiveMoney]"
|
||||
@update:modelValue="onMoneyInput('totalAmount', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="editData.currency"
|
||||
:items="currencyOptions"
|
||||
label="واحد پول"
|
||||
:rules="[rules.required]"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
class="ltr-input"
|
||||
:model-value="formatMoney(editData.exchangeRate)"
|
||||
label="نرخ تبدیل (ریال)"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:rules="[rules.exchangeRateRule]"
|
||||
@update:modelValue="onMoneyInput('exchangeRate', $event)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(editData.totalAmountIRR)"
|
||||
label="مبلغ کل (ریال)"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="editData.description"
|
||||
label="توضیحات"
|
||||
rows="3"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveChanges"
|
||||
:loading="saveLoading"
|
||||
:disabled="!isFormValidForSave"
|
||||
>
|
||||
ذخیره تغییرات
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
<div v-else>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-3">
|
||||
<strong>نام تامین کننده:</strong>
|
||||
<div>{{ workflow.supplierName || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-3">
|
||||
<strong>کشور تامین کننده:</strong>
|
||||
<div>{{ workflow.supplierCountry || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-3">
|
||||
<strong>تلفن تامین کننده:</strong>
|
||||
<div>{{ workflow.supplierPhone || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-3">
|
||||
<strong>ایمیل تامین کننده:</strong>
|
||||
<div>{{ workflow.supplierEmail || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="mb-3">
|
||||
<strong>آدرس تامین کننده:</strong>
|
||||
<div>{{ workflow.supplierAddress || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<div class="mb-3">
|
||||
<strong>مبلغ کل:</strong>
|
||||
<div>{{ formatMoney(workflow.totalAmount) }} {{ workflow.currency }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<div class="mb-3">
|
||||
<strong>نرخ تبدیل:</strong>
|
||||
<div>{{ formatMoney(workflow.exchangeRate) }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<div class="mb-3">
|
||||
<strong>مبلغ کل (ریال):</strong>
|
||||
<div>{{ formatMoney(workflow.totalAmountIRR) }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="mb-3">
|
||||
<strong>توضیحات:</strong>
|
||||
<div>{{ workflow.description || '-' }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title>اطلاعات سیستمی</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="mb-3">
|
||||
<strong>کد پرونده:</strong>
|
||||
<div>{{ workflow.code }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>تاریخ ثبت:</strong>
|
||||
<div>{{ formatDate(workflow.dateSubmit) }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>ثبت کننده:</strong>
|
||||
<div>{{ workflow.submitter }}</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Tabs -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-tabs v-model="activeTab" bg-color="primary">
|
||||
<v-tab value="items">آیتمها ({{ workflow.items?.length || 0 }})</v-tab>
|
||||
<v-tab value="payments">پرداختها ({{ workflow.payments?.length || 0 }})</v-tab>
|
||||
<v-tab value="documents">اسناد ({{ workflow.documents?.length || 0 }})</v-tab>
|
||||
<v-tab value="stages">مراحل ({{ workflow.stages?.length || 0 }})</v-tab>
|
||||
<v-tab value="shipping">حمل و نقل ({{ workflow.shipping?.length || 0 }})</v-tab>
|
||||
<v-tab value="customs">ترخیص ({{ workflow.customs?.length || 0 }})</v-tab>
|
||||
<v-tab value="tickets">حوالههای مرتبط</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window v-model="activeTab">
|
||||
<v-tabs-window-item value="items">
|
||||
<ImportWorkflowItems
|
||||
:workflow-id="workflowId"
|
||||
:items="workflow.items"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="payments">
|
||||
<ImportWorkflowPayments
|
||||
:workflow-id="workflowId"
|
||||
:payments="workflow.payments"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="documents">
|
||||
<ImportWorkflowDocuments
|
||||
:workflow-id="workflowId"
|
||||
:documents="workflow.documents"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="stages">
|
||||
<ImportWorkflowStages
|
||||
:workflow-id="workflowId"
|
||||
:stages="workflow.stages"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="shipping">
|
||||
<ImportWorkflowShipping
|
||||
:workflow-id="workflowId"
|
||||
:shipping="workflow.shipping"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="customs">
|
||||
<ImportWorkflowCustoms
|
||||
:workflow-id="workflowId"
|
||||
:customs="workflow.customs"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="tickets">
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center mb-4 gap-2">
|
||||
<v-select
|
||||
v-model="ticketsStatusFilter"
|
||||
:items="ticketStatusOptions"
|
||||
label="فیلتر وضعیت"
|
||||
style="max-width: 260px"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="loadRelatedTickets"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="text" icon="mdi-refresh" @click="loadRelatedTickets" :loading="loadingTickets" />
|
||||
</div>
|
||||
<v-data-table
|
||||
:headers="ticketsHeaders"
|
||||
:header-props="{ class: 'custom-header' }"
|
||||
:items="relatedTickets"
|
||||
:loading="loadingTickets"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #item.code="{ item }">
|
||||
<v-chip color="secondary" variant="tonal" size="small">{{ item.code }}</v-chip>
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<v-chip :color="ticketStatusColor(item.status)" size="small">
|
||||
{{ ticketStatusLabel(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="$router.push({ name: 'storeroom_ticket_view', params: { id: item.code } })"
|
||||
>
|
||||
مشاهده
|
||||
</v-btn>
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn v-bind="props" icon variant="text" size="small">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="st in ticketStatusOptions" :key="st.value" @click="updateTicketStatus(item.code, st.value)" :disabled="!st.value">
|
||||
<v-list-item-title>{{ st.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<!-- Dialog: Create inbound storeroom ticket -->
|
||||
<v-dialog v-model="showCreateTicketDialog" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title>ایجاد حواله ورود به انبار</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="selectedStoreroomId"
|
||||
:items="storerooms"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
label="انبار"
|
||||
:loading="loadingStorerooms"
|
||||
:disabled="loadingStorerooms"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex gap-2">
|
||||
<v-text-field
|
||||
v-model="personSearch"
|
||||
label="جستجوی طرفحساب"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn color="primary" @click="searchPersons" :loading="loadingPersons">جستجو</v-btn>
|
||||
</div>
|
||||
<v-select
|
||||
class="mt-3"
|
||||
v-model="selectedPersonId"
|
||||
:items="persons"
|
||||
item-title="nikename"
|
||||
item-value="id"
|
||||
label="انتخاب طرفحساب"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="showCreateTicketDialog = false">انصراف</v-btn>
|
||||
<v-btn color="success" @click="createInboundTicket" :loading="creatingTicket" :disabled="!canCreateTicket">ایجاد حواله</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import ImportWorkflowItems from '../../../../components/plugins/import-workflow/ImportWorkflowItems.vue'
|
||||
import ImportWorkflowPayments from '../../../../components/plugins/import-workflow/ImportWorkflowPayments.vue'
|
||||
import ImportWorkflowDocuments from '../../../../components/plugins/import-workflow/ImportWorkflowDocuments.vue'
|
||||
import ImportWorkflowStages from '../../../../components/plugins/import-workflow/ImportWorkflowStages.vue'
|
||||
import ImportWorkflowShipping from '../../../../components/plugins/import-workflow/ImportWorkflowShipping.vue'
|
||||
import ImportWorkflowCustoms from '../../../../components/plugins/import-workflow/ImportWorkflowCustoms.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const workflowId = route.params.id
|
||||
|
||||
// Data
|
||||
const workflow = ref(null)
|
||||
const form = ref(null)
|
||||
const loading = ref(false)
|
||||
const editMode = ref(false)
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const activeTab = ref('items')
|
||||
// Create inbound ticket dialog
|
||||
const showCreateTicketDialog = ref(false)
|
||||
const storerooms = ref([])
|
||||
const loadingStorerooms = ref(false)
|
||||
const selectedStoreroomId = ref(null)
|
||||
const personSearch = ref('')
|
||||
const persons = ref([])
|
||||
const loadingPersons = ref(false)
|
||||
const selectedPersonId = ref(null)
|
||||
const creatingTicket = ref(false)
|
||||
|
||||
const ticketsHeaders = [
|
||||
{ title: 'کد حواله', key: 'code' },
|
||||
{ title: 'تاریخ', key: 'date' },
|
||||
{ title: 'نوع', key: 'typeString' },
|
||||
{ title: 'وضعیت', key: 'status' },
|
||||
{ title: 'انبار', key: 'storeroom' },
|
||||
{ title: 'طرف حساب', key: 'person' },
|
||||
{ title: 'عملیات', key: 'actions', sortable: false },
|
||||
]
|
||||
const ticketStatusOptions = [
|
||||
{ title: 'همه', value: null },
|
||||
{ title: 'در جریان', value: 'in_progress' },
|
||||
{ title: 'تایید شده', value: 'approved' },
|
||||
{ title: 'انجام شده', value: 'done' },
|
||||
{ title: 'رد شده', value: 'rejected' },
|
||||
{ title: 'در انتظار تایید', value: 'pending_approval' },
|
||||
]
|
||||
const ticketsStatusFilter = ref(null)
|
||||
const relatedTickets = ref([])
|
||||
const loadingTickets = ref(false)
|
||||
|
||||
const canCreateTicket = computed(() => !!selectedStoreroomId.value && !!selectedPersonId.value)
|
||||
|
||||
const openCreateTicketDialog = async () => {
|
||||
showCreateTicketDialog.value = true
|
||||
await loadStorerooms()
|
||||
}
|
||||
|
||||
const loadStorerooms = async () => {
|
||||
try {
|
||||
loadingStorerooms.value = true
|
||||
const res = await axios.get('/api/storeroom/list/active')
|
||||
// انتظار میرود سرورها آرایهای از انبارها بدهد
|
||||
storerooms.value = Array.isArray(res.data?.data) ? res.data.data : (Array.isArray(res.data) ? res.data : [])
|
||||
} catch (e) {
|
||||
storerooms.value = []
|
||||
} finally {
|
||||
loadingStorerooms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchPersons = async () => {
|
||||
try {
|
||||
loadingPersons.value = true
|
||||
const res = await axios.post('/api/person/list/search', { search: personSearch.value || '' })
|
||||
persons.value = Array.isArray(res.data) ? res.data : (Array.isArray(res.data?.Result) ? res.data.Result : [])
|
||||
} catch (e) {
|
||||
persons.value = []
|
||||
} finally {
|
||||
loadingPersons.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createInboundTicket = async () => {
|
||||
if (!canCreateTicket.value || !workflow.value) return
|
||||
try {
|
||||
creatingTicket.value = true
|
||||
const res = await axios.post(`/api/import-workflow/${workflow.value.code}/create-inbound-ticket`, {
|
||||
storeroom_id: selectedStoreroomId.value,
|
||||
person_id: selectedPersonId.value,
|
||||
})
|
||||
if (res.data?.Success) {
|
||||
Swal.fire({ title: 'موفق', text: `حواله با کد ${res.data.Result.ticketCode} ایجاد شد`, icon: 'success' })
|
||||
showCreateTicketDialog.value = false
|
||||
await loadRelatedTickets()
|
||||
} else {
|
||||
throw new Error(res.data?.ErrorMessage || 'خطا در ایجاد حواله')
|
||||
}
|
||||
} catch (e) {
|
||||
Swal.fire({ title: 'خطا', text: 'ایجاد حواله ناموفق بود', icon: 'error' })
|
||||
} finally {
|
||||
creatingTicket.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadRelatedTickets = async () => {
|
||||
if (!workflow.value) return
|
||||
try {
|
||||
loadingTickets.value = true
|
||||
const qs = ticketsStatusFilter.value ? `?status=${ticketsStatusFilter.value}` : ''
|
||||
const res = await axios.get(`/api/storeroom/tickets${qs}`)
|
||||
const items = Array.isArray(res.data) ? res.data : []
|
||||
relatedTickets.value = items.filter(t => t.importWorkflowCode === workflow.value.code)
|
||||
} catch (e) {
|
||||
relatedTickets.value = []
|
||||
} finally {
|
||||
loadingTickets.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateTicketStatus = async (code, status) => {
|
||||
try {
|
||||
await axios.post(`/api/storeroom/ticket/status/${code}`, { status })
|
||||
await loadRelatedTickets()
|
||||
Swal.fire({ title: 'موفق', text: 'وضعیت حواله بهروزرسانی شد', icon: 'success' })
|
||||
} catch (e) {
|
||||
Swal.fire({ title: 'خطا', text: 'بهروزرسانی وضعیت ناموفق بود', icon: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const editData = ref({})
|
||||
|
||||
// Currency options
|
||||
const currencyOptions = [
|
||||
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||
]
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||
minLength: (value) => !value || value.length >= 3 || 'حداقل 3 کاراکتر الزامی است',
|
||||
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
||||
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
||||
positiveMoney: (value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||
},
|
||||
exchangeRateRule: (value) => {
|
||||
// اگر واحد پول IRR باشد، خالی بودن یا 1 بودن نرخ تبدیل را مجاز کن تا فرم معتبر بماند
|
||||
const currentCurrency = editData.value?.currency
|
||||
const numeric = parseMoneyInput(value)
|
||||
if (currentCurrency === 'IRR') {
|
||||
return numeric >= 0 || true
|
||||
}
|
||||
return numeric > 0 || 'نرخ تبدیل باید بزرگتر از صفر باشد'
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for total amount and exchange rate changes
|
||||
watch(
|
||||
() => [editData.value.totalAmount, editData.value.exchangeRate, editData.value.currency],
|
||||
([totalAmount, exchangeRate, currency]) => {
|
||||
const total = parseFloat(totalAmount) || 0
|
||||
const computedRate = currency === 'IRR' ? 1 : (parseFloat(exchangeRate) || 0)
|
||||
const result = Math.round(total * computedRate)
|
||||
editData.value.totalAmountIRR = isNaN(result) ? 0 : result
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Methods
|
||||
const loadWorkflow = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get(`/api/import-workflow/${workflowId}`)
|
||||
|
||||
if (response.data.Success) {
|
||||
workflow.value = response.data.Result
|
||||
editData.value = { ...response.data.Result }
|
||||
|
||||
// Trigger the watch manually after setting editData
|
||||
// مقدار ریالی بر اساس مبلغ و نرخ تبدیل/واحد پول محاسبه میشود
|
||||
const total = parseFloat(editData.value.totalAmount) || 0
|
||||
const rate = editData.value.currency === 'IRR' ? 1 : (parseFloat(editData.value.exchangeRate) || 0)
|
||||
editData.value.totalAmountIRR = Math.round(total * rate)
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading workflow:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در بارگذاری جزئیات پرونده واردات خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveChanges = async () => {
|
||||
if (!isFormValidForSave.value) return
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
const response = await axios.put(`/api/import-workflow/${workflowId}/update`, editData.value)
|
||||
|
||||
if (response.data.Success) {
|
||||
Swal.fire({
|
||||
title: 'موفق',
|
||||
text: 'تغییرات با موفقیت ذخیره شد',
|
||||
icon: 'success'
|
||||
})
|
||||
editMode.value = false
|
||||
loadWorkflow()
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving changes:', error)
|
||||
Swal.fire({
|
||||
title: 'خطا',
|
||||
text: 'در ذخیره تغییرات خطایی رخ داد',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const parseMoneyInput = (val) => {
|
||||
if (val === null || val === undefined) return 0
|
||||
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||
const num = Number(cleaned)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
}
|
||||
|
||||
// دکمه ذخیره تنها زمانی فعال شود که فیلدهای کلیدی معتبر باشند
|
||||
const isFormValidForSave = computed(() => {
|
||||
const titleOk = typeof editData.value.title === 'string' && editData.value.title.trim().length >= 3
|
||||
const supplierOk = typeof editData.value.supplierName === 'string' && editData.value.supplierName.trim().length >= 3
|
||||
const currencyOk = !!editData.value.currency
|
||||
const total = parseMoneyInput(editData.value.totalAmount)
|
||||
const rate = parseMoneyInput(editData.value.exchangeRate)
|
||||
const totalOk = total >= 0
|
||||
const rateOk = editData.value.currency === 'IRR' ? rate >= 0 : rate > 0
|
||||
return titleOk && supplierOk && currencyOk && totalOk && rateOk && valid.value
|
||||
})
|
||||
|
||||
const onMoneyInput = (field, value) => {
|
||||
const numeric = parseMoneyInput(value)
|
||||
editData.value[field] = numeric
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
draft: 'grey',
|
||||
processing: 'blue',
|
||||
shipped: 'orange',
|
||||
arrived: 'purple',
|
||||
cleared: 'teal',
|
||||
completed: 'green',
|
||||
cancelled: 'red'
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
draft: 'پیشنویس',
|
||||
processing: 'در حال پردازش',
|
||||
shipped: 'ارسال شده',
|
||||
arrived: 'رسیده',
|
||||
cleared: 'ترخیص شده',
|
||||
completed: 'تکمیل شده',
|
||||
cancelled: 'لغو شده'
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
|
||||
// نمایش مبالغ بدون اعشار و با جداکننده ویرگول بین هر سه رقم
|
||||
const formatMoney = (value) => {
|
||||
const numericValue = Number(value) || 0
|
||||
return numericValue
|
||||
.toFixed(0)
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleDateString('fa-IR')
|
||||
}
|
||||
|
||||
// استایل وضعیت حوالهها
|
||||
const ticketStatusLabel = (status) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'تایید شده'
|
||||
case 'pending_approval': return 'در انتظار تایید'
|
||||
case 'in_progress': return 'در حال انجام'
|
||||
case 'done': return 'انجام شده'
|
||||
case 'rejected': return 'رد شده'
|
||||
default: return status || '-'
|
||||
}
|
||||
}
|
||||
const ticketStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'success'
|
||||
case 'pending_approval': return 'warning'
|
||||
case 'in_progress': return 'info'
|
||||
case 'done': return 'primary'
|
||||
case 'rejected': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for edit mode changes
|
||||
watch(editMode, async (newVal) => {
|
||||
if (newVal && workflow.value) {
|
||||
editData.value = { ...workflow.value }
|
||||
await nextTick()
|
||||
if (form.value && typeof form.value.validate === 'function') {
|
||||
form.value.validate()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await loadWorkflow()
|
||||
await loadRelatedTickets()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-workflow-detail {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.ltr-input :deep(input) {
|
||||
direction: ltr !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
|
@ -39,6 +39,12 @@
|
|||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-subheader color="primary">وضعیت تایید</v-list-subheader>
|
||||
<v-list-item class="text-dark" title="تایید فاکتورهای انتخابی" @click="approveSelectedInvoices">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="green" icon="mdi-check-decagram"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-subheader color="primary">{{ $t('dialog.change_labels') }}</v-list-subheader>
|
||||
<v-list-item v-for="item in types" class="text-dark" :title="$t('dialog.change_to') + ' ' + item.label" @click="changeLabel(item)">
|
||||
<template v-slot:prepend>
|
||||
|
@ -419,6 +425,29 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
approveSelectedInvoices() {
|
||||
if (this.itemsSelected.length === 0) {
|
||||
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
|
||||
return;
|
||||
}
|
||||
Swal.fire({ title: 'تایید فاکتورهای انتخابی', text: 'فاکتورهای انتخابشده تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' })
|
||||
.then(async (r) => {
|
||||
if (!r.isConfirmed) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
for (const code of this.itemsSelected) {
|
||||
await axios.post(`/api/sell/approve/${code}`);
|
||||
}
|
||||
Swal.fire({ text: 'فاکتورها تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
|
||||
this.itemsSelected = [];
|
||||
this.loadData();
|
||||
} catch (e) {
|
||||
Swal.fire({ text: 'خطا در تایید فاکتورها', icon: 'error', confirmButtonText: 'قبول' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
isPluginActive(pluginCode) {
|
||||
return this.plugins && this.plugins[pluginCode] !== undefined;
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts">
|
||||
<script>
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import Rec from '../component/rec.vue';
|
||||
|
@ -46,12 +46,14 @@ export default defineComponent({
|
|||
text: '',
|
||||
color: 'error'
|
||||
},
|
||||
sendSerialsLoading: false,
|
||||
item: {
|
||||
doc: { id: 0, date: null, code: null, des: '', amount: 0, profit: 0, shortLink: null },
|
||||
relatedDocs: [],
|
||||
rows: [],
|
||||
},
|
||||
person: { nikename: null, mobile: '', tel: '', addres: '', postalcode: '' },
|
||||
warrantySerials: [],
|
||||
person: { id: null, nikename: null, mobile: '', tel: '', addres: '', postalcode: '' },
|
||||
commoditys: [],
|
||||
totalRec: 0,
|
||||
totalDiscount: 0,
|
||||
|
@ -103,6 +105,33 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
copyWarrantySerials() {
|
||||
const list = this.warrantySerials || [];
|
||||
const text = list.map((s) => s.serialNumber).join(', ');
|
||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
this.snackbar = { show: true, text: 'سریالها کپی شد', color: 'success' };
|
||||
},
|
||||
async sendWarrantySerialsBySms() {
|
||||
if (!this.person.mobile) {
|
||||
this.snackbar = { show: true, text: 'موبایل مشتری ثبت نشده است', color: 'error' };
|
||||
return;
|
||||
}
|
||||
if (!this.item?.doc?.code) return;
|
||||
this.sendSerialsLoading = true;
|
||||
try {
|
||||
const res = await axios.post('/api/plugins/warranty/send-serials', {
|
||||
invoiceCode: this.item.doc.code,
|
||||
mobile: this.person.mobile
|
||||
});
|
||||
this.snackbar = { show: true, text: 'سریالها برای مشتری ارسال شد', color: 'success' };
|
||||
} catch (e) {
|
||||
this.snackbar = { show: true, text: 'خطا در ارسال سریالها', color: 'error' };
|
||||
} finally {
|
||||
this.sendSerialsLoading = false;
|
||||
}
|
||||
},
|
||||
async checkCanEdit() {
|
||||
try {
|
||||
const response = await axios.get(`/api/sell/edit/can/${this.$route.params.id}`);
|
||||
|
@ -131,7 +160,13 @@ export default defineComponent({
|
|||
this.shortlink_url = this.item.doc.shortLink
|
||||
? `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.shortLink}`
|
||||
: `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.id}`;
|
||||
this.totalRec = response.data.relatedDocs.reduce((sum: number, rdoc: any) => sum + parseInt(rdoc.amount), 0);
|
||||
this.totalRec = response.data.relatedDocs.reduce((sum, rdoc) => sum + parseInt(rdoc.amount), 0);
|
||||
// دریافت سریالهای گارانتی مرتبط با فاکتور
|
||||
axios.get(`/api/plugins/warranty/serials/by-invoice/${this.item.doc.code}`).then((res) => {
|
||||
this.warrantySerials = res.data?.items || [];
|
||||
}).catch(() => {
|
||||
this.warrantySerials = [];
|
||||
});
|
||||
});
|
||||
|
||||
axios.get(`/api/sell/v2/get/${this.$route.params.id}`).then((response) => {
|
||||
|
@ -151,7 +186,7 @@ export default defineComponent({
|
|||
this.totalTax = 0;
|
||||
this.totalDiscount = 0;
|
||||
|
||||
this.commoditys = data.items.map((item: any) => {
|
||||
this.commoditys = data.items.map((item) => {
|
||||
this.totalTax += item.tax;
|
||||
this.totalDiscount += item.discountAmount;
|
||||
return {
|
||||
|
@ -215,6 +250,14 @@ export default defineComponent({
|
|||
/>
|
||||
|
||||
<share-options v-if="bid.shortlinks" :shortlink-url="shortlink_url" :mobile="person.mobile" :invoice-id="item.doc.id" />
|
||||
<v-btn icon :disabled="!warrantySerials.length" @click="copyWarrantySerials">
|
||||
<v-icon>mdi-clipboard-text</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">کپی سریالهای گارانتی</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon :loading="sendSerialsLoading" :disabled="!warrantySerials.length || !person.mobile" @click="sendWarrantySerialsBySms">
|
||||
<v-icon>mdi-message-processing</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">ارسال سریالهای گارانتی با SMS</v-tooltip>
|
||||
</v-btn>
|
||||
<print-options :invoice-id="$route.params.id" />
|
||||
</v-toolbar>
|
||||
|
||||
|
@ -338,6 +381,14 @@ export default defineComponent({
|
|||
<!-- تب دریافتها -->
|
||||
<v-window-item value="payments">
|
||||
<v-card-text>
|
||||
<v-alert v-if="warrantySerials.length" type="info" variant="tonal" class="mb-3">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
سریالهای گارانتی تخصیص یافته: {{ warrantySerials.map(s=>s.serialNumber).join('، ') }}
|
||||
</div>
|
||||
<v-btn size="small" color="primary" @click="copyWarrantySerials">کپی سریالها</v-btn>
|
||||
</div>
|
||||
</v-alert>
|
||||
<v-data-table v-if="item.relatedDocs.length" :header-props="{ class: 'custom-header' }" :headers="[
|
||||
{ title: 'مشاهده', key: 'view' },
|
||||
{ title: 'شماره', key: 'code' },
|
||||
|
|
|
@ -71,6 +71,12 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteTicket('output', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
|
@ -82,7 +88,10 @@
|
|||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||
</tr>
|
||||
|
@ -112,6 +121,12 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteTicket('input', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
|
@ -123,7 +138,10 @@
|
|||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||
</tr>
|
||||
|
@ -153,6 +171,12 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteTicket('transfer', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
|
@ -164,7 +188,10 @@
|
|||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||
</tr>
|
||||
|
@ -194,6 +221,12 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteTicket('waste', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
|
@ -203,9 +236,12 @@
|
|||
</v-list>
|
||||
</v-menu>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||
</tr>
|
||||
|
@ -265,6 +301,7 @@ import axios from "axios";
|
|||
interface Ticket {
|
||||
code: string;
|
||||
date: string;
|
||||
status?: string;
|
||||
doc: {
|
||||
code: string;
|
||||
};
|
||||
|
@ -277,7 +314,7 @@ interface Ticket {
|
|||
interface Header {
|
||||
title: string;
|
||||
key: string;
|
||||
align: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
sortable: boolean;
|
||||
width: number;
|
||||
visible: boolean;
|
||||
|
@ -311,17 +348,18 @@ const snackbar = ref({
|
|||
|
||||
// تعریف همه ستونها
|
||||
const allHeaders = ref<Header[]>([
|
||||
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
|
||||
{ title: "شماره", key: "code", align: 'center', sortable: true, width: 100, visible: true },
|
||||
{ title: "تاریخ", key: "date", align: 'center', sortable: true, width: 120, visible: true },
|
||||
{ title: "شماره فاکتور", key: "doc.code", align: 'center', sortable: true, width: 120, visible: true },
|
||||
{ title: "شخص", key: "person.nikename", align: 'center', sortable: true, width: 120, visible: true },
|
||||
{ title: "توضیحات", key: "des", align: 'center', sortable: true, width: 200, visible: true },
|
||||
{ title: "عملیات", key: "operation", align: 'center' as const, sortable: false, width: 100, visible: true },
|
||||
{ title: "شماره", key: "code", align: 'center' as const, sortable: true, width: 100, visible: true },
|
||||
{ title: "تاریخ", key: "date", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "وضعیت", key: "status", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شماره فاکتور", key: "doc.code", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شخص", key: "person.nikename", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "توضیحات", key: "des", align: 'center' as const, sortable: true, width: 200, visible: true },
|
||||
]);
|
||||
|
||||
// ستونهای قابل نمایش
|
||||
const visibleHeaders = computed(() => {
|
||||
return allHeaders.value.filter((header: Header) => header.visible);
|
||||
return allHeaders.value.filter((header: Header) => header.visible) as any;
|
||||
});
|
||||
|
||||
// بررسی نمایش ستون
|
||||
|
@ -367,10 +405,10 @@ const loadData = async () => {
|
|||
axios.post('/api/storeroom/tickets/list/transfer'),
|
||||
axios.post('/api/storeroom/tickets/list/waste')
|
||||
]);
|
||||
inputItems.value = inputResponse.data;
|
||||
outputItems.value = outputResponse.data;
|
||||
transferItems.value = transferResponse.data;
|
||||
wasteItems.value = wasteResponse.data;
|
||||
inputItems.value = (inputResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
outputItems.value = (outputResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
transferItems.value = (transferResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
wasteItems.value = (wasteResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
} catch (error: any) {
|
||||
console.error('Error loading data:', error);
|
||||
snackbar.value = {
|
||||
|
@ -383,6 +421,47 @@ const loadData = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// تایید حواله
|
||||
const approveTicket = async (code: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await axios.post(`/api/storeroom/ticket/status/${code}`, { status: 'approved' });
|
||||
// بهروزرسانی وضعیت در لیستهای محلی
|
||||
[inputItems, outputItems, transferItems, wasteItems].forEach(listRef => {
|
||||
const idx = listRef.value.findIndex((x: any) => x.code === code);
|
||||
if (idx !== -1) listRef.value[idx].status = 'approved';
|
||||
});
|
||||
snackbar.value = { show: true, message: 'حواله تایید شد', color: 'success' };
|
||||
} catch (error: any) {
|
||||
snackbar.value = { show: true, message: 'خطا در تایید حواله: ' + (error.response?.data?.message || error.message), color: 'error' };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// نمایش برچسب وضعیت
|
||||
const statusLabel = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'تایید شده';
|
||||
case 'pending_approval': return 'در انتظار تایید';
|
||||
case 'in_progress': return 'در حال انجام';
|
||||
case 'done': return 'انجام شده';
|
||||
case 'rejected': return 'رد شده';
|
||||
default: return status || '-';
|
||||
}
|
||||
};
|
||||
|
||||
const statusColor = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'success';
|
||||
case 'pending_approval': return 'warning';
|
||||
case 'in_progress': return 'info';
|
||||
case 'done': return 'primary';
|
||||
case 'rejected': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// حذف حواله
|
||||
const deleteTicket = (type: 'input' | 'output' | 'transfer' | 'waste', code: string) => {
|
||||
deleteDialog.value = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
@ -128,8 +128,56 @@ const printInvoice = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Attachments
|
||||
const attachments = ref<any[]>([])
|
||||
const loadingAttachments = ref(false)
|
||||
const uploading = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const attachDes = ref('')
|
||||
|
||||
const canPrint = computed(() => {
|
||||
// اگر نیاز به تایید دو مرحلهای وجود داشته باشد بکاند 403 میدهد، اما برای UX بهتر اینجا هم کنترل کنیم.
|
||||
// اگر وضعیت حواله pending_approval باشد، چاپ را غیرفعال کنیم
|
||||
const st = (item.value as any)?.ticket?.status as string | undefined
|
||||
if (!st) return true
|
||||
return st === 'approved' || st === 'done' || st === 'in_progress'
|
||||
})
|
||||
|
||||
const loadAttachments = async () => {
|
||||
try {
|
||||
loadingAttachments.value = true
|
||||
const res = await axios.get(`/api/storeroom/ticket/attachments/${router.currentRoute.value.params.id}`)
|
||||
attachments.value = Array.isArray(res.data) ? res.data : []
|
||||
} catch (e) {
|
||||
attachments.value = []
|
||||
} finally {
|
||||
loadingAttachments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAttachment = async () => {
|
||||
if (!selectedFile.value) return
|
||||
try {
|
||||
uploading.value = true
|
||||
const fd = new FormData()
|
||||
fd.append('file', selectedFile.value)
|
||||
if (attachDes.value) fd.append('des', attachDes.value)
|
||||
await axios.post(`/api/storeroom/ticket/attachments/upload/${router.currentRoute.value.params.id}`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
selectedFile.value = null
|
||||
attachDes.value = ''
|
||||
await loadAttachments()
|
||||
} catch (e) {
|
||||
// خطا نمایش داده نمیشود تا ساده بماند
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAttachments()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -150,6 +198,7 @@ onMounted(() => {
|
|||
@click="printInvoice"
|
||||
color="primary"
|
||||
icon="mdi-printer"
|
||||
:disabled="!canPrint"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
@ -293,6 +342,53 @@ onMounted(() => {
|
|||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card variant="outlined" class="mt-4">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
<v-icon start>mdi-paperclip</v-icon>
|
||||
پیوستها
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center gap-2 mb-3">
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
label="انتخاب فایل"
|
||||
accept="image/*,.pdf,.jpg,.jpeg,.png"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
style="max-width: 360px"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="attachDes"
|
||||
label="توضیحات"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn color="primary" :loading="uploading" :disabled="!selectedFile" @click="uploadAttachment">آپلود</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" icon="mdi-refresh" :loading="loadingAttachments" @click="loadAttachments" />
|
||||
</div>
|
||||
<v-data-table
|
||||
:headers="[
|
||||
{ title: 'نام فایل', key: 'filename' },
|
||||
{ title: 'نوع', key: 'fileType' },
|
||||
{ title: 'حجم', key: 'fileSize' },
|
||||
{ title: 'توضیحات', key: 'des' },
|
||||
{ title: 'تاریخ', key: 'dateSubmit' },
|
||||
]"
|
||||
:items="attachments"
|
||||
:loading="loadingAttachments"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #item.filename="{ item }">
|
||||
<a :href="item.filename" target="_blank" rel="noreferrer noopener">{{ item.filename }}</a>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in a new issue