This commit is contained in:
Gloomy 2025-08-11 11:50:24 +00:00
parent ccc3fb3c6f
commit a19175cfb4
25 changed files with 5056 additions and 110 deletions

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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);
}
}

View file

@ -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']]);

View file

@ -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');

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -198,7 +198,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) {
@ -789,20 +789,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>

View file

@ -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>
@ -241,10 +354,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 },
@ -318,17 +443,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') });
@ -409,6 +567,16 @@ export default {
return labels[type] || type;
},
},
computed: {
snackbar: {
get() {
return this.$store.state.snackbar;
},
set(value) {
this.$store.commit('setSnackbar', value);
}
}
}
};
</script>

View file

@ -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) {

View file

@ -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">
@ -385,6 +392,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({

View 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>

View 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>

View file

@ -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;
},

View file

@ -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' },

View file

@ -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 = {

View file

@ -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>