update two-step system

This commit is contained in:
Gloomy 2025-08-19 20:47:11 +00:00
parent f137fcb0dc
commit 68bd621a58
9 changed files with 489 additions and 281 deletions

View file

@ -29,7 +29,7 @@ class ApprovalController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response { ): Response {
try { try {
$acc = $access->hasRole('settings'); $acc = $access->hasRole('store');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
@ -82,6 +82,68 @@ class ApprovalController extends AbstractController
} }
} }
#[Route('/api/approval/unapprove/storeroom/{ticketCode}', name: 'api_approval_unapprove_storeroom', methods: ['POST'])]
public function unapproveStoreroomTicket(
$ticketCode,
#[CurrentUser] ?User $user,
Access $access,
LogService $logService,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('store');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
if (!$businessSettings->isRequireTwoStepApproval()) {
return $this->json(['success' => false, 'message' => 'تأیید دو مرحله‌ای فعال نیست']);
}
$ticket = $entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findOneBy([
'code' => $ticketCode,
'bid' => $business
]);
if (!$ticket) {
return $this->json(['success' => false, 'message' => 'حواله انبار یافت نشد']);
}
$canApprove = $this->canUserApproveStoreroomTicket($user, $businessSettings);
if (!$canApprove) {
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
}
$ticket->setIsPreview(true);
$ticket->setIsApproved(false);
$ticket->setApprovedBy(null);
$entityManager->persist($ticket);
$entityManager->flush();
$logService->insert(
'لغو تأیید حواله انبار',
"حواله انبار {$ticket->getCode()} توسط {$user->getFullName()} لغو تأیید شد",
$user,
$business
);
return $this->json([
'success' => true,
'message' => 'حواله انبار با موفقیت لغو تأیید شد'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در لغو تأیید حواله انبار: ' . $e->getMessage()
], 500);
}
}
#[Route('/api/approval/approve/sales/{docId}', name: 'api_approval_approve_sales', methods: ['POST'])] #[Route('/api/approval/approve/sales/{docId}', name: 'api_approval_approve_sales', methods: ['POST'])]
public function approveSalesInvoice( public function approveSalesInvoice(
$docId, $docId,
@ -91,7 +153,7 @@ class ApprovalController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response { ): Response {
try { try {
$acc = $access->hasRole('settings'); $acc = $access->hasRole('sell');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
@ -166,7 +228,7 @@ class ApprovalController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response { ): Response {
try { try {
$acc = $access->hasRole('settings'); $acc = $access->hasRole('sell');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
@ -250,7 +312,7 @@ class ApprovalController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response { ): Response {
try { try {
$acc = $access->hasRole('settings'); $acc = $access->hasRole('sell');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
@ -316,50 +378,6 @@ class ApprovalController extends AbstractController
} }
} }
#[Route('/api/approval/check-permission/{docId}', name: 'api_approval_check_permission', methods: ['GET'])]
public function checkApprovalPermission(
$docId,
#[CurrentUser] ?User $user,
Access $access,
EntityManagerInterface $entityManager
): Response {
try {
$acc = $access->hasRole('settings');
if (!$acc) {
return $this->json(['canApprove' => false, 'message' => 'دسترسی محدود']);
}
$business = $acc['bid'];
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneByIncludePreview([
'id' => $docId,
'bid' => $business
]);
if (!$document) {
return $this->json(['canApprove' => false, 'message' => 'سند یافت نشد']);
}
$canApprove = $this->canUserApproveDocument($user, $businessSettings, $document);
return $this->json([
'canApprove' => $canApprove,
'documentStatus' => [
'isPreview' => $document->isPreview(),
'isApproved' => $document->isApproved(),
'approvedBy' => $document->getApprovedBy() ? $document->getApprovedBy()->getFullName() : null
]
]);
} catch (\Exception $e) {
return $this->json([
'canApprove' => false,
'message' => 'خطا در بررسی مجوز: ' . $e->getMessage()
], 500);
}
}
private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool
{ {
if ($user->getEmail() === $business->getOwner()->getEmail()) { if ($user->getEmail() === $business->getOwner()->getEmail()) {
@ -370,11 +388,9 @@ class ApprovalController extends AbstractController
switch ($documentType) { switch ($documentType) {
case 'invoice': case 'invoice':
return $business->getInvoiceApprover() === $user->getEmail(); return $business->getApproverSellInvoice() === $user->getEmail();
case 'warehouse': case 'warehouse':
return $business->getWarehouseApprover() === $user->getEmail(); return $business->getApproverWarehouseTransfer() === $user->getEmail();
case 'financial':
return $business->getFinancialApprover() === $user->getEmail();
default: default:
return false; return false;
} }
@ -405,7 +421,7 @@ class ApprovalController extends AbstractController
return true; return true;
} }
return $business->getWarehouseApprover() === $user->getEmail(); return $business->getApproverWarehouseTransfer() === $user->getEmail();
} }
private function canUserApproveSalesInvoice(User $user, Business $business): bool private function canUserApproveSalesInvoice(User $user, Business $business): bool
@ -414,15 +430,6 @@ class ApprovalController extends AbstractController
return true; return true;
} }
return $business->getInvoiceApprover() === $user->getEmail(); return $business->getApproverSellInvoice() === $user->getEmail();
}
private function canUserApproveFinancialDocument(User $user, Business $business): bool
{
if ($user->getEmail() === $business->getOwner()->getEmail()) {
return true;
}
return $business->getFinancialApprover() === $user->getEmail();
} }
} }

View file

@ -103,7 +103,21 @@ class BusinessController extends AbstractController
]); ]);
if (!$perms) if (!$perms)
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
return $this->json(Explore::ExploreBusiness($bus)); $result = Explore::ExploreBusiness($bus);
// Read approval settings from Business entity (only new fields)
$result['approvers'] = [
'sellInvoice' => $bus->getApproverSellInvoice(),
'buyInvoice' => $bus->getApproverBuyInvoice(),
'returnBuy' => $bus->getApproverReturnBuy(),
'returnSell' => $bus->getApproverReturnSell(),
'warehouseTransfer' => $bus->getApproverWarehouseTransfer(),
'receiveFromPersons' => $bus->getApproverReceiveFromPersons(),
'payToPersons' => $bus->getApproverPayToPersons(),
'accountingDocs' => $bus->getApproverAccountingDocs(),
'bankTransfers' => $bus->getApproverBankTransfers(),
];
return $this->json($result);
} }
#[Route('/api/business/list/count', name: 'api_bussiness_list_count')] #[Route('/api/business/list/count', name: 'api_bussiness_list_count')]
@ -246,32 +260,26 @@ class BusinessController extends AbstractController
$business->setWalletEnable(false); $business->setWalletEnable(false);
} }
} }
if (array_key_exists('requireTwoStepApproval', $params)) {
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval']);
}
// Set approvers // Approval settings
if (array_key_exists('invoiceApprover', $params)) { $business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval'] ?? false);
$business->setInvoiceApprover($params['invoiceApprover']);
}
if (array_key_exists('warehouseApprover', $params)) { $approvers = $params['approvers'] ?? [];
$business->setWarehouseApprover($params['warehouseApprover']);
}
if (array_key_exists('financialApprover', $params)) { $business->setApproverSellInvoice($approvers['sellInvoice'] ?? null);
$business->setFinancialApprover($params['financialApprover']); $business->setApproverBuyInvoice($approvers['buyInvoice'] ?? null);
} $business->setApproverReturnBuy($approvers['returnBuy'] ?? null);
$business->setApproverReturnSell($approvers['returnSell'] ?? null);
$business->setApproverWarehouseTransfer($approvers['warehouseTransfer'] ?? null);
$business->setApproverReceiveFromPersons($approvers['receiveFromPersons'] ?? null);
$business->setApproverPayToPersons($approvers['payToPersons'] ?? null);
$business->setApproverAccountingDocs($approvers['accountingDocs'] ?? null);
$business->setApproverBankTransfers($approvers['bankTransfers'] ?? null);
if (array_key_exists('requireWarrantyOnDelivery', $params)) { // Warranty settings
$business->setRequireWarrantyOnDelivery($params['requireWarrantyOnDelivery']); $business->setRequireWarrantyOnDelivery($params['requireWarrantyOnDelivery'] ?? false);
} $business->setActivationGraceDays($params['activationGraceDays'] ?? 7);
if (array_key_exists('activationGraceDays', $params)) { $business->setMatchWarrantyToSerial($params['matchWarrantyToSerial'] ?? false);
$business->setActivationGraceDays($params['activationGraceDays']);
}
if (array_key_exists('matchWarrantyToSerial', $params)) {
$business->setMatchWarrantyToSerial($params['matchWarrantyToSerial']);
}
//get Money type //get Money type
if (!array_key_exists('arzmain', $params) && $isNew) { if (!array_key_exists('arzmain', $params) && $isNew) {
@ -287,9 +295,12 @@ class BusinessController extends AbstractController
$business->setDateSubmit(time()); $business->setDateSubmit(time());
$entityManager->persist($business); $entityManager->persist($business);
$entityManager->flush(); $entityManager->flush();
// No registry usage; settings persisted on Business entity
if ($isNew) { if ($isNew) {
$perms = new Permission(); $perms = new Permission();
$giftCredit = (int) $registryMGR->get('system_settings', 'gift_credit', 0); $giftCreditRaw = $registryMGR->get('system_settings', 'gift_credit');
$giftCredit = (int) ($giftCreditRaw ?? 0);
$business->setSmsCharge($giftCredit); $business->setSmsCharge($giftCredit);
$perms->setBid($business); $perms->setBid($business);
$perms->setUser($this->getUser()); $perms->setUser($this->getUser());

View file

@ -316,14 +316,39 @@ class Business
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?bool $requireTwoStepApproval = null; private ?bool $requireTwoStepApproval = null;
#[ORM\Column(length: 255, nullable: true)] // Two-step approval extended configuration
private ?string $invoiceApprover = null; #[ORM\Column(nullable: true)]
private ?bool $approvalUseSameApprover = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $warehouseApprover = null; private ?string $approverAll = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $financialApprover = null; private ?string $approverSellInvoice = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverBuyInvoice = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReturnBuy = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReturnSell = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverWarehouseTransfer = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverReceiveFromPersons = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverPayToPersons = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverAccountingDocs = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $approverBankTransfers = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?bool $requireWarrantyOnDelivery = null; private ?bool $requireWarrantyOnDelivery = null;
@ -2231,36 +2256,102 @@ class Business
return $this; return $this;
} }
public function getInvoiceApprover(): ?string public function getApproverSellInvoice(): ?string
{ {
return $this->invoiceApprover; return $this->approverSellInvoice;
} }
public function setInvoiceApprover(?string $invoiceApprover): static public function setApproverSellInvoice(?string $approverSellInvoice): static
{ {
$this->invoiceApprover = $invoiceApprover; $this->approverSellInvoice = $approverSellInvoice;
return $this; return $this;
} }
public function getWarehouseApprover(): ?string public function getApproverBuyInvoice(): ?string
{ {
return $this->warehouseApprover; return $this->approverBuyInvoice;
} }
public function setWarehouseApprover(?string $warehouseApprover): static public function setApproverBuyInvoice(?string $approverBuyInvoice): static
{ {
$this->warehouseApprover = $warehouseApprover; $this->approverBuyInvoice = $approverBuyInvoice;
return $this; return $this;
} }
public function getFinancialApprover(): ?string public function getApproverReturnBuy(): ?string
{ {
return $this->financialApprover; return $this->approverReturnBuy;
} }
public function setFinancialApprover(?string $financialApprover): static public function setApproverReturnBuy(?string $approverReturnBuy): static
{ {
$this->financialApprover = $financialApprover; $this->approverReturnBuy = $approverReturnBuy;
return $this;
}
public function getApproverReturnSell(): ?string
{
return $this->approverReturnSell;
}
public function setApproverReturnSell(?string $approverReturnSell): static
{
$this->approverReturnSell = $approverReturnSell;
return $this;
}
public function getApproverWarehouseTransfer(): ?string
{
return $this->approverWarehouseTransfer;
}
public function setApproverWarehouseTransfer(?string $approverWarehouseTransfer): static
{
$this->approverWarehouseTransfer = $approverWarehouseTransfer;
return $this;
}
public function getApproverReceiveFromPersons(): ?string
{
return $this->approverReceiveFromPersons;
}
public function setApproverReceiveFromPersons(?string $approverReceiveFromPersons): static
{
$this->approverReceiveFromPersons = $approverReceiveFromPersons;
return $this;
}
public function getApproverPayToPersons(): ?string
{
return $this->approverPayToPersons;
}
public function setApproverPayToPersons(?string $approverPayToPersons): static
{
$this->approverPayToPersons = $approverPayToPersons;
return $this;
}
public function getApproverAccountingDocs(): ?string
{
return $this->approverAccountingDocs;
}
public function setApproverAccountingDocs(?string $approverAccountingDocs): static
{
$this->approverAccountingDocs = $approverAccountingDocs;
return $this;
}
public function getApproverBankTransfers(): ?string
{
return $this->approverBankTransfers;
}
public function setApproverBankTransfers(?string $approverBankTransfers): static
{
$this->approverBankTransfers = $approverBankTransfers;
return $this; return $this;
} }

View file

@ -579,9 +579,17 @@ class Explore
'walletEnabled' => $item->isWalletEnable(), 'walletEnabled' => $item->isWalletEnable(),
'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null, 'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null,
'requireTwoStepApproval' => $item->isRequireTwoStepApproval(), 'requireTwoStepApproval' => $item->isRequireTwoStepApproval(),
'invoiceApprover' => $item->getInvoiceApprover(), 'approvers' => [
'warehouseApprover' => $item->getWarehouseApprover(), 'sellInvoice' => $item->getApproverSellInvoice(),
'financialApprover' => $item->getFinancialApprover(), 'buyInvoice' => $item->getApproverBuyInvoice(),
'returnBuy' => $item->getApproverReturnBuy(),
'returnSell' => $item->getApproverReturnSell(),
'warehouseTransfer' => $item->getApproverWarehouseTransfer(),
'receiveFromPersons' => $item->getApproverReceiveFromPersons(),
'payToPersons' => $item->getApproverPayToPersons(),
'accountingDocs' => $item->getApproverAccountingDocs(),
'bankTransfers' => $item->getApproverBankTransfers(),
],
'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(), 'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(),
'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(), 'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(),
'requireWarrantyOnDelivery' => $item->getRequireWarrantyOnDelivery(), 'requireWarrantyOnDelivery' => $item->getRequireWarrantyOnDelivery(),

View file

@ -24,11 +24,9 @@ export function canApproveDocument(businessSettings, currentUserEmail, isBusines
// Check specific approver based on document type // Check specific approver based on document type
switch (documentType) { switch (documentType) {
case 'invoice': case 'invoice':
return businessSettings.invoiceApprover === currentUserEmail; return businessSettings.approvers.sellInvoice === currentUserEmail;
case 'warehouse': case 'warehouse':
return businessSettings.warehouseApprover === currentUserEmail; return businessSettings.approvers.warehouseTransfer === currentUserEmail;
case 'financial':
return businessSettings.financialApprover === currentUserEmail;
default: default:
return false; return false;
} }

View file

@ -34,9 +34,9 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-subheader color="primary" v-if="business.requireTwoStepApproval">وضعیت تایید</v-list-subheader> <v-list-subheader color="primary" v-if="checkApprover()">وضعیت تایید</v-list-subheader>
<v-list-item class="text-dark" title="تایید فاکتورهای انتخابی" @click="approveSelectedInvoices" <v-list-item class="text-dark" title="تایید فاکتورهای انتخابی" @click="approveSelectedInvoices"
v-if="business.requireTwoStepApproval"> v-if="checkApprover()">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="green" icon="mdi-check-decagram"></v-icon> <v-icon color="green" icon="mdi-check-decagram"></v-icon>
</template> </template>
@ -404,7 +404,7 @@ export default defineComponent({
invoiceIndex: true invoiceIndex: true
}, },
plugins: {}, plugins: {},
business: { requireTwoStepApproval: false, invoiceApprover: null }, business: { requireTwoStepApproval: false, approvers: { sellInvoice: null } },
currentUser: { email: '', owner: false }, currentUser: { email: '', owner: false },
sumSelected: 0, sumSelected: 0,
sumSelectedProfit: 0, sumSelectedProfit: 0,
@ -507,6 +507,9 @@ export default defineComponent({
isPluginActive(pluginCode) { isPluginActive(pluginCode) {
return this.plugins && this.plugins[pluginCode] !== undefined; return this.plugins && this.plugins[pluginCode] !== undefined;
}, },
checkApprover() {
return this.business.requireTwoStepApproval && (this.business.approvers.sellInvoice == this.currentUser.email || this.currentUser.owner === true);
},
async loadPlugins() { async loadPlugins() {
try { try {
const response = await axios.post('/api/plugin/get/actives'); const response = await axios.post('/api/plugin/get/actives');
@ -519,10 +522,10 @@ export default defineComponent({
async loadBusinessInfo() { async loadBusinessInfo() {
try { try {
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid')); const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
this.business = response.data || { requireTwoStepApproval: false, invoiceApprover: null }; this.business = response.data || { requireTwoStepApproval: false, approvers: { sellInvoice: null } };
} catch (error) { } catch (error) {
console.error('Error loading business info:', error); console.error('Error loading business info:', error);
this.business = { requireTwoStepApproval: false, invoiceApprover: null }; this.business = { requireTwoStepApproval: false, approvers: { sellInvoice: null } };
} }
}, },
async loadCurrentUser() { async loadCurrentUser() {
@ -639,11 +642,11 @@ export default defineComponent({
return 'success'; return 'success';
}, },
canShowApprovalButton(item) { canShowApprovalButton(item) {
if (!this.business?.requireTwoStepApproval) return false; if (!this.checkApprover()) return false;
if (item?.isApproved || (!item?.isPreview && !item?.isApproved)) return false; if (item?.isApproved) return false;
return this.business?.invoiceApprover === this.currentUser?.email || this.currentUser?.owner === true; return true;
}, },
async approveInvoice(code) { async approveInvoice(code) {
try { try {
@ -660,7 +663,7 @@ export default defineComponent({
} }
}, },
canShowUnapproveButton(item) { canShowUnapproveButton(item) {
return !this.canShowApprovalButton(item); return !this.canShowApprovalButton(item) && this.checkApprover();
}, },
async unapproveInvoice(code) { async unapproveInvoice(code) {
try { try {

View file

@ -24,10 +24,13 @@
<v-tab value="2"> <v-tab value="2">
{{ $t('dialog.global_settings') }} {{ $t('dialog.global_settings') }}
</v-tab> </v-tab>
<v-tab value="3" v-if="isPluginActive('warranty')"> <v-tab value="3" v-if="isPluginActive('accpro')">
تایید اسناد
</v-tab>
<v-tab value="4" v-if="isPluginActive('warranty')">
{{ $t('dialog.warranty_settings') }} {{ $t('dialog.warranty_settings') }}
</v-tab> </v-tab>
<v-tab value="4" v-if="showBackupTab"> <v-tab value="5" v-if="showBackupTab">
نسخه پشتیبان نسخه پشتیبان
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -198,7 +201,170 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="3" v-if="isPluginActive('warranty')">
<v-tabs-window-item value="3" v-if="isPluginActive('accpro')">
<v-card>
<v-card-text>
<h3 class="text-primary mb-6">تایید اسناد</h3>
<v-row>
<v-col cols="12" md="8">
<v-card variant="outlined" class="mb-6">
<v-card-text>
<v-switch
v-model="content.requireTwoStepApproval"
label="فعال‌سازی تایید دومرحله‌ای برای اسناد"
color="primary"
hide-details
class="mb-2"
></v-switch>
<div class="text-body-2 text-medium-emphasis mb-2">
با فعالسازی این گزینه، تمام اسناد انتخابی نیاز به تایید خواهند داشت.
</div>
<v-expand-transition>
<div v-if="content.requireTwoStepApproval">
<v-divider class="my-4"></v-divider>
<h4 class="text-subtitle-1 mb-3">تعیین تاییدکنندگان</h4>
<v-row>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.sellInvoice"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده فاکتور فروش"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.buyInvoice"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده فاکتور خرید"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.returnBuy"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده برگشت از خرید"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.returnSell"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده برگشت از فروش"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.warehouseTransfer"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده حواله انبار"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.receiveFromPersons"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده دریافت از اشخاص"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.payToPersons"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده پرداخت به اشخاص"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.accountingDocs"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده اسناد حسابداری"
variant="outlined"
density="compact"
clearable
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="content.approvers.bankTransfers"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده انتقال‌ها (بانکداری)"
variant="outlined"
density="compact"
clearable
/>
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-4">
<v-icon size="small" color="info" class="me-1">mdi-information</v-icon>
در صورت عدم انتخاب تاییدکننده برای هر بخش، فقط مدیر کسب و کار مجاز به تایید است.
</div>
</div>
</v-expand-transition>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined">
<v-card-title class="text-h6 text-primary">
<v-icon icon="mdi-information-outline" class="mr-2"></v-icon>
نکات
</v-card-title>
<v-card-text>
<v-list density="compact">
<v-list-item title="صاحب کسب‌وکار همیشه مجاز به تایید همه اسناد است." />
<v-list-item title="می‌توانید یک تاییدکننده برای همه یا تاییدکننده‌های مجزا تعیین کنید." />
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-tabs-window-item>
<v-tabs-window-item value="4" v-if="isPluginActive('warranty')">
<v-card> <v-card>
<v-card-text> <v-card-text>
<h3 class="text-primary mb-6">تنظیمات گارانتی</h3> <h3 class="text-primary mb-6">تنظیمات گارانتی</h3>
@ -370,126 +536,7 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<!-- تایید دومرحلهای -->
<v-card v-if="isPluginActive('accpro')" variant="outlined" class="mb-6">
<v-card-title class="text-h6 text-primary">
<v-icon icon="mdi-shield-check" class="mr-2"></v-icon>
تایید دومرحلهای
</v-card-title>
<v-card-text>
<v-switch
v-model="content.requireTwoStepApproval"
label="فعال‌سازی تایید دومرحله‌ای برای اسناد"
color="primary"
hide-details
class="mb-2"
></v-switch>
<div class="text-body-2 text-medium-emphasis mb-2">
با فعالسازی این گزینه، تمام فاکتورها، حوالههای انبار، دریافتها و پرداختها نیاز به تایید مدیر خواهند داشت.
</div>
<v-expand-transition>
<div v-if="content.requireTwoStepApproval">
<v-divider class="my-4"></v-divider>
<h4 class="text-subtitle-1 mb-3">تعیین تاییدکنندگان</h4>
<v-row>
<v-col cols="12">
<v-select
v-model="content.invoiceApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده فاکتور فروش"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند فاکتورهای فروش را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند فاکتورهای فروش را تایید کند
</div>
</v-col>
<v-col cols="12">
<v-select
v-model="content.warehouseApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده حواله انبار"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند حواله‌های انبار را تایید کند"
persistent-hint
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon
:color="item.owner ? 'success' : 'primary'"
size="small"
>
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
</v-icon>
</template>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<div class="text-caption text-medium-emphasis mt-1">
این کاربر افزون بر مدیر کسب و کار میتواند حوالههای انبار را تایید کند
</div>
</v-col>
<!-- تاییدکننده دریافت و پرداخت مالی (غیرفعال فعلاً) -->
<!--
<v-col cols="12">
<v-select
v-model="content.financialApprover"
:items="users"
item-title="name"
item-value="email"
label="تاییدکننده دریافت و پرداخت مالی"
variant="outlined"
density="compact"
clearable
hint="کاربری که می‌تواند دریافت‌ها و پرداخت‌ها را تایید کند"
persistent-hint
/>
</v-col>
-->
</v-row>
<div class="text-caption text-medium-emphasis">
<v-icon size="small" color="info" class="me-1">mdi-information</v-icon>
در صورت عدم انتخاب تاییدکننده، فقط مدیر کسب و کار میتواند اسناد را تایید کند
</div>
<div class="text-caption text-medium-emphasis mt-2">
<v-icon size="small" color="success" class="me-1">mdi-check-circle</v-icon>
<strong>نکته:</strong> صاحب کسب و کار همیشه میتواند تمام اسناد را تأیید کند و نیازی به تعیین مجدد ندارد
</div>
</div>
</v-expand-transition>
</v-card-text>
</v-card>
<v-card variant="outlined"> <v-card variant="outlined">
<v-card-title class="text-h6 text-primary"> <v-card-title class="text-h6 text-primary">
@ -594,7 +641,7 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="4" v-if="showBackupTab"> <v-tabs-window-item value="5" v-if="showBackupTab">
<v-card> <v-card>
<v-card-text> <v-card-text>
<h3 class="text-primary mb-4">نسخه پشتیبان از اطلاعات کسب و کار</h3> <h3 class="text-primary mb-4">نسخه پشتیبان از اطلاعات کسب و کار</h3>
@ -743,9 +790,17 @@ export default {
walletEnabled: false, walletEnabled: false,
walletMatchBank: null, walletMatchBank: null,
requireTwoStepApproval: false, requireTwoStepApproval: false,
invoiceApprover: null, approvers: {
warehouseApprover: null, sellInvoice: null,
financialApprover: null, buyInvoice: null,
returnBuy: null,
returnSell: null,
warehouseTransfer: null,
receiveFromPersons: null,
payToPersons: null,
accountingDocs: null,
bankTransfers: null
},
year: { year: {
startShamsi: '', startShamsi: '',
endShamsi: '', endShamsi: '',
@ -871,9 +926,7 @@ export default {
'walletEnabled': this.content.walletEnabled, 'walletEnabled': this.content.walletEnabled,
'walletMatchBank': this.content.walletMatchBank, 'walletMatchBank': this.content.walletMatchBank,
'requireTwoStepApproval': this.content.requireTwoStepApproval, 'requireTwoStepApproval': this.content.requireTwoStepApproval,
'invoiceApprover': this.content.invoiceApprover, 'approvers': this.content.approvers,
'warehouseApprover': this.content.warehouseApprover,
'financialApprover': this.content.financialApprover,
'year': this.content.year, 'year': this.content.year,
'commodityUpdateBuyPriceAuto': this.content.updateBuyPrice, 'commodityUpdateBuyPriceAuto': this.content.updateBuyPrice,
'commodityUpdateSellPriceAuto': this.content.updateSellPrice, 'commodityUpdateSellPriceAuto': this.content.updateSellPrice,
@ -924,19 +977,11 @@ export default {
this.content.walletMatchBank = this.content.walletMatchBank.id; this.content.walletMatchBank = this.content.walletMatchBank.id;
} }
// اطمینان از وجود فیلدهای تأییدکننده // اطمینان از وجود فیلدهای تایید اسناد
if (!this.content.hasOwnProperty('invoiceApprover')) { if (!this.content.hasOwnProperty('requireTwoStepApproval')) this.content.requireTwoStepApproval = false;
this.content.invoiceApprover = null; if (!this.content.hasOwnProperty('approvers')) this.content.approvers = {};
} const approverKeys = ['sellInvoice','buyInvoice','returnBuy','returnSell','warehouseTransfer','receiveFromPersons','payToPersons','accountingDocs','bankTransfers'];
if (!this.content.hasOwnProperty('warehouseApprover')) { approverKeys.forEach(k => { if (!this.content.approvers.hasOwnProperty(k)) this.content.approvers[k] = null; });
this.content.warehouseApprover = null;
}
if (!this.content.hasOwnProperty('financialApprover')) {
this.content.financialApprover = null;
}
if (!this.content.hasOwnProperty('requireTwoStepApproval')) {
this.content.requireTwoStepApproval = false;
}
// سپس سایر دادهها را بارگذاری کن // سپس سایر دادهها را بارگذاری کن
const [moneyResponse, banksResponse, usersResponse, pluginsResponse] = await Promise.all([ const [moneyResponse, banksResponse, usersResponse, pluginsResponse] = await Promise.all([

View file

@ -81,6 +81,12 @@
</template> </template>
<v-list-item-title>تایید حواله</v-list-item-title> <v-list-item-title>تایید حواله</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" @click="unapproveTicket(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
<v-list-item-title>لغو تایید حواله</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteTicket('output', item.code)"> <v-list-item @click="deleteTicket('output', item.code)">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">mdi-delete</v-icon>
@ -140,6 +146,12 @@
</template> </template>
<v-list-item-title>تایید حواله</v-list-item-title> <v-list-item-title>تایید حواله</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" @click="unapproveTicket(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
<v-list-item-title>لغو تایید حواله</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteTicket('input', item.code)"> <v-list-item @click="deleteTicket('input', item.code)">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">mdi-delete</v-icon>
@ -199,6 +211,12 @@
</template> </template>
<v-list-item-title>تایید حواله</v-list-item-title> <v-list-item-title>تایید حواله</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" @click="unapproveTicket(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
<v-list-item-title>لغو تایید حواله</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteTicket('transfer', item.code)"> <v-list-item @click="deleteTicket('transfer', item.code)">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">mdi-delete</v-icon>
@ -258,6 +276,12 @@
</template> </template>
<v-list-item-title>تایید حواله</v-list-item-title> <v-list-item-title>تایید حواله</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="canShowUnapproveButton(item)" @click="unapproveTicket(item.code)">
<template v-slot:prepend>
<v-icon color="red">mdi-cancel</v-icon>
</template>
<v-list-item-title>لغو تایید حواله</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteTicket('waste', item.code)"> <v-list-item @click="deleteTicket('waste', item.code)">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">mdi-delete</v-icon>
@ -381,7 +405,7 @@ const wasteSubTab = ref<'approved' | 'pending'>('approved');
const showColumnDialog = ref(false); const showColumnDialog = ref(false);
const business = ref({ const business = ref({
requireTwoStepApproval: false, requireTwoStepApproval: false,
warehouseApprover: null approvers: { warehouseTransfer: null }
}); });
const currentUser = ref({ email: '', owner: false }); const currentUser = ref({ email: '', owner: false });
@ -483,10 +507,10 @@ const displayWasteItems = computed(() => {
const loadBusinessInfo = async () => { const loadBusinessInfo = async () => {
try { try {
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid')); const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
business.value = response.data || { requireTwoStepApproval: false, warehouseApprover: null }; business.value = response.data || { requireTwoStepApproval: false, approvers: { warehouseTransfer: null } };
} catch (error: any) { } catch (error: any) {
console.error('Error loading business info:', error); console.error('Error loading business info:', error);
business.value = { requireTwoStepApproval: false, warehouseApprover: null }; business.value = { requireTwoStepApproval: false, approvers: { warehouseTransfer: null } };
} }
}; };
@ -525,12 +549,18 @@ const loadData = async () => {
} }
}; };
const checkApprover = () => {
return business.value.requireTwoStepApproval && (business.value.approvers.warehouseTransfer === currentUser.value.email || currentUser.value.owner === true);
};
const canShowApprovalButton = (item: Ticket) => { const canShowApprovalButton = (item: Ticket) => {
if (!business.value.requireTwoStepApproval) return false; if (!checkApprover()) return false;
if (item?.approved) return false; if (item?.approved) return false;
return true;
return business.value.warehouseApprover === currentUser.value.email || currentUser.value.owner === true; };
const canShowUnapproveButton = (item: Ticket) => {
return !canShowApprovalButton(item) && checkApprover();
}; };
const approveTicket = async (code: string) => { const approveTicket = async (code: string) => {
@ -548,6 +578,21 @@ const approveTicket = async (code: string) => {
} }
}; };
const unapproveTicket = async (code: string) => {
try {
loading.value = true;
await axios.post(`/api/approval/unapprove/storeroom/${code}`);
await loadData();
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 getApprovalStatusText = (item: Ticket) => { const getApprovalStatusText = (item: Ticket) => {
if (!business.value.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال'; if (!business.value.requireTwoStepApproval) return 'تایید دو مرحله‌ای غیرفعال';

View file

@ -1290,31 +1290,31 @@ export default {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.input-section >>> .v-field__field { .input-section :deep(.v-field__field) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.custom-input >>> .v-input__control { .custom-input :deep(.v-input__control) {
min-height: 60px; min-height: 60px;
} }
.custom-input >>> .v-text-field__details { .custom-input :deep(.v-text-field__details) {
margin-top: 8px; margin-top: 8px;
} }
.custom-input >>> .v-input__slot { .custom-input :deep(.v-input__slot) {
border-radius: 12px !important; border-radius: 12px !important;
background: #f8fafc; background: #f8fafc;
border: 2px solid #e2e8f0 !important; border: 2px solid #e2e8f0 !important;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.custom-input >>> .v-input__slot:hover { .custom-input :deep(.v-input__slot:hover) {
border-color: #3b82f6 !important; border-color: #3b82f6 !important;
} }
.custom-input >>> .v-text-field--focused .v-input__slot { .custom-input :deep(.v-text-field--focused .v-input__slot) {
border-color: #3b82f6 !important; border-color: #3b82f6 !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
@ -1342,7 +1342,7 @@ export default {
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
} }
.primary-btn >>> .v-btn__content { .primary-btn :deep(.v-btn__content) {
gap: 8px !important; gap: 8px !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
@ -1459,7 +1459,7 @@ export default {
font-size: 1.5rem; font-size: 1.5rem;
} }
.custom-input >>> .v-input__control { .custom-input :deep(.v-input__control) {
min-height: 50px; min-height: 50px;
} }
@ -1833,7 +1833,7 @@ export default {
min-height: 40px !important; min-height: 40px !important;
} }
.footer-link-btn >>> .v-btn__content { .footer-link-btn :deep(.v-btn__content) {
gap: 8px !important; gap: 8px !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
@ -2210,7 +2210,7 @@ export default {
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
} }
.back-btn >>> .v-btn__content { .back-btn :deep(.v-btn__content) {
gap: 8px !important; gap: 8px !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
@ -2234,7 +2234,7 @@ export default {
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
} }
.success-btn >>> .v-btn__content { .success-btn :deep(.v-btn__content) {
gap: 8px !important; gap: 8px !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
@ -2256,7 +2256,7 @@ export default {
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
} }
.error-btn >>> .v-btn__content { .error-btn :deep(.v-btn__content) {
gap: 8px !important; gap: 8px !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;