update core

This commit is contained in:
Hesabix 2025-04-24 17:08:02 +00:00
parent 3ad815ec7f
commit 04550d2171
18 changed files with 12139 additions and 277 deletions

View file

@ -4,56 +4,56 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"php": ">=8.2",
"ext-ctype": "*",
"ext-curl": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"doctrine/annotations": "^1.0",
"doctrine/dbal": "^3.9",
"doctrine/doctrine-bundle": "^2.8",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14",
"dompdf/dompdf": "^2.0",
"melipayamak/php": "1.0.0",
"doctrine/annotations": "^2.0",
"doctrine/dbal": "^4.2",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.2",
"dompdf/dompdf": "^3.0",
"melipayamak/php": "^1.0",
"mpdf/mpdf": "^8.2",
"nelmio/api-doc-bundle": "^4.34",
"nelmio/api-doc-bundle": "^4.35",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.3",
"phpoffice/phpspreadsheet": "^1.29",
"phpstan/phpdoc-parser": "^1.16",
"phpdocumentor/reflection-docblock": "^5.4",
"phpoffice/phpspreadsheet": "^2.3",
"phpstan/phpdoc-parser": "^1.33",
"ramsey/uuid": "^4.7",
"symfony/apache-pack": "^1.0",
"symfony/asset": "7.1.*",
"symfony/console": "7.1.*",
"symfony/doctrine-messenger": "7.1.*",
"symfony/dotenv": "7.1.*",
"symfony/expression-language": "7.1.*",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",
"symfony/doctrine-messenger": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/expression-language": "7.2.*",
"symfony/flex": "^2",
"symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*",
"symfony/http-client": "7.1.*",
"symfony/lock": "7.1.*",
"symfony/mailer": "7.1.*",
"symfony/mime": "7.1.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.1.*",
"symfony/process": "7.1.*",
"symfony/property-access": "7.1.*",
"symfony/property-info": "7.1.*",
"symfony/runtime": "7.1.*",
"symfony/security-bundle": "7.1.*",
"symfony/serializer": "7.1.*",
"symfony/string": "7.1.*",
"symfony/translation": "7.1.*",
"symfony/twig-bundle": "7.1.*",
"symfony/validator": "7.1.*",
"symfony/web-link": "7.1.*",
"symfony/yaml": "7.1.*",
"symfonycasts/verify-email-bundle": "^1.13",
"tecnickcom/tcpdf": "^6.6",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/http-client": "7.2.*",
"symfony/lock": "7.2.*",
"symfony/mailer": "7.2.*",
"symfony/mime": "7.2.*",
"symfony/monolog-bundle": "^3.10",
"symfony/notifier": "7.2.*",
"symfony/process": "7.2.*",
"symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/serializer": "7.2.*",
"symfony/string": "7.2.*",
"symfony/translation": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/validator": "7.2.*",
"symfony/web-link": "7.2.*",
"symfony/yaml": "7.2.*",
"symfonycasts/verify-email-bundle": "^1.17",
"tecnickcom/tcpdf": "^6.7",
"twig/extra-bundle": "^3.14",
"twig/twig": "^3.14"
},
"config": {
"allow-plugins": {
@ -99,19 +99,19 @@
"extra": {
"symfony": {
"allow-contrib": true,
"require": "7.1.*",
"require": "7.2.*",
"docker": true
},
"public-dir": "../public_html"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.1.*",
"symfony/css-selector": "7.1.*",
"symfony/debug-bundle": "7.1.*",
"symfony/maker-bundle": "^1.48",
"symfony/phpunit-bridge": "^7.1",
"symfony/stopwatch": "7.1.*",
"symfony/web-profiler-bundle": "7.1.*"
"phpunit/phpunit": "^11.4",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "7.2.*",
"symfony/maker-bundle": "^1.62",
"symfony/phpunit-bridge": "^7.2",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*"
}
}

11385
hesabixCore/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View file

@ -19,6 +19,7 @@ use App\Entity\Cashdesk;
use App\Entity\Salary;
use App\Entity\Person;
use App\Service\Log;
use Doctrine\Common\Collections\ArrayCollection;
class CostController extends AbstractController
{
@ -138,7 +139,10 @@ class CostController extends AbstractController
->andWhere('r.bd != 0')
->groupBy('t.id, t.name')
->orderBy('total_cost', 'DESC')
->setParameters($parameters);
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year']);
// اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') {

View file

@ -1134,4 +1134,36 @@ class HesabdariController extends AbstractController
return $this->json($extractor->operationSuccess($pdfPid));
}
#[Route('/api/hesabdari/tables/{id}/children', name: 'get_hesabdari_table_children', methods: ['GET'])]
public function getHesabdariTableChildren(int $id, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$node = $entityManager->getRepository(HesabdariTable::class)->find($id);
if (!$node) {
return $this->json(['Success' => false, 'message' => 'نود مورد نظر یافت نشد'], 404);
}
$children = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $node,
'bid' => [$acc['bid']->getId(), null] // حساب‌های عمومی و خصوصی
]);
$result = [];
foreach ($children as $child) {
$result[] = [
'id' => $child->getId(),
'name' => $child->getName(),
'code' => $child->getCode(),
'type' => $child->getType(),
'children' => $this->hasChild($entityManager, $child) ? [] : null
];
}
return $this->json(['Success' => true, 'data' => $result]);
}
}

View file

@ -9,6 +9,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\Common\Collections\ArrayCollection;
class IncomeController extends AbstractController
{
@ -111,14 +112,6 @@ class IncomeController extends AbstractController
$today = $jdate->jdate('Y/m/d', time());
$monthStart = $jdate->jdate('Y/m/01', time());
// پارامترهای پایه
$parameters = [
'bid' => $acc['bid'],
'money' => $acc['money'],
'type' => 'income',
'year' => $acc['year'],
];
// کوئری پایه
$qb = $entityManager->createQueryBuilder()
->select('t.name AS center_name, SUM(COALESCE(r.bs, 0)) AS total_income')
@ -132,7 +125,10 @@ class IncomeController extends AbstractController
->andWhere('r.bs != 0') // فقط ردیف‌هایی که bs صفر نیست
->groupBy('t.id, t.name')
->orderBy('total_income', 'DESC')
->setParameters($parameters);
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'income')
->setParameter('year', $acc['year']);
// اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') {

View file

@ -5,24 +5,24 @@ use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
class Cast extends FunctionNode
{
private $expression;
public function parse(Parser $parser)
public function parse(Parser $parser): void
{
$parser->match(Lexer::T_IDENTIFIER); // CAST
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$parser->match(TokenType::T_IDENTIFIER); // CAST
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->expression = $parser->ArithmeticExpression();
$parser->match(Lexer::T_AS);
$parser->match(Lexer::T_IDENTIFIER); // INTEGER یا هر نوع دیگه
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
$parser->match(TokenType::T_AS);
$parser->match(TokenType::T_IDENTIFIER); // INTEGER یا هر نوع دیگه
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker)
public function getSql(SqlWalker $sqlWalker): string
{
// به جای استفاده از $this->type، مستقیماً SIGNED رو می‌ذاریم
return 'CAST(' . $sqlWalker->walkArithmeticPrimary($this->expression) . ' AS SIGNED)';
}
}

View file

@ -39,7 +39,7 @@ class HesabdariDoc
#[ORM\Column(length: 255, nullable: true)]
private ?string $type = null;
#[ORM\Column(type: Types::BIGINT)]
#[ORM\Column(type: Types::STRING, length: 255)]
private ?string $code = null;
#[ORM\ManyToOne(inversedBy: 'hesabdariDocs')]
@ -50,8 +50,8 @@ class HesabdariDoc
#[ORM\Column(length: 255, nullable: true)]
private ?string $des = null;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private int $amount = 0;
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $amount = 0;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
@ -94,7 +94,7 @@ class HesabdariDoc
#[Ignore]
private Collection $storeroomTickets;
#[ORM\Column(type: Types::ARRAY , nullable: true)]
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $tempStatus = null;
#[ORM\OneToMany(mappedBy: 'doc', targetEntity: Log::class)]
@ -375,7 +375,7 @@ class HesabdariDoc
return $this;
}
/**
/**
* @return Collection<int, self>
*/
public function getRelatedDocs(): Collection
@ -399,7 +399,6 @@ class HesabdariDoc
return $this;
}
public function getStatus(): ?string
{
return $this->status;
@ -573,4 +572,4 @@ class HesabdariDoc
return $this;
}
}
}

View file

@ -59,8 +59,8 @@ class HesabdariRow
#[Ignore]
private ?Commodity $commodity = null;
#[ORM\Column(type: 'string', length: 255,nullable: true)]
private ?string $commdityCount = null;
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $commdityCount = null;
#[ORM\ManyToOne(inversedBy: 'hesabdariRows')]
#[Ignore]
@ -79,7 +79,7 @@ class HesabdariRow
#[ORM\Column(length: 255, nullable: true)]
private ?string $plugin = null;
#[ORM\Column(type: Types::ARRAY, nullable: true)]
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $tempData = null;
#[ORM\ManyToOne(inversedBy: 'hesabdariRows')]
@ -91,12 +91,8 @@ class HesabdariRow
#[ORM\Column(length: 255, nullable: true)]
private ?string $tax = null;
public function __construct()
{
}
public function getId(): ?int
@ -224,12 +220,12 @@ class HesabdariRow
return $this;
}
public function getCommdityCount(): ?string
public function getCommdityCount(): ?int
{
return $this->commdityCount;
}
public function setCommdityCount(?string $commdityCount): self
public function setCommdityCount(?int $commdityCount): self
{
$this->commdityCount = $commdityCount;
@ -343,4 +339,4 @@ class HesabdariRow
return $this;
}
}
}

View file

@ -122,6 +122,18 @@
"./.env"
]
},
"symfony/form": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": {
"version": "6.2",
"recipe": {

View file

@ -1,6 +1,6 @@
{
"name": "hesabix",
"version": "0.45.0",
"version": "0.48.0",
"private": true,
"scripts": {
"dev": "vite",
@ -10,68 +10,62 @@
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@chenfengyuan/vue-countdown": "^2.1.2",
"@ckeditor/ckeditor5-build-classic": "^36.0.1",
"@ckeditor/ckeditor5-image": "^36.0.1",
"@ckeditor/ckeditor5-vue": "^4.0.1",
"@ckeditor/vite-plugin-ckeditor5": "^0.1.1",
"@chenfengyuan/vue-countdown": "^2.1.3",
"@date-io/date-fns-jalali": "^3.2.0",
"@mdi/font": "^7.4.47",
"@syncfusion/ej2-vue-dropdowns": "^21.2.5",
"@syncfusion/ej2-vue-dropdowns": "^29.1.38",
"@tiptap/extension-text-align": "^2.11.7",
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@tiptap/vue-3": "^2.11.7",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"@vueuse/core": "^12.0.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^13.1.0",
"animate.css": "^4.1.1",
"apexcharts": "^4.4.0",
"axios": "^1.2.3",
"apexcharts": "^4.6.0",
"axios": "^1.8.4",
"date-fns": "^4.1.0",
"date-fns-jalali": "^3.2.0-0",
"downloadjs": "^1.4.7",
"file-saver": "^2.0.5",
"jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.10.44",
"libphonenumber-js": "^1.12.7",
"lodash": "^4.17.21",
"maska": "^3.0.4",
"maz-ui": "^3.11.4",
"pinia": "^2.2.6",
"sweetalert2": "^11.6.13",
"v-money3": "^3.24.0",
"v-skeleton-loader": "^0.1.9",
"maska": "^3.1.1",
"maz-ui": "^3.50.1",
"pinia": "^3.0.2",
"sweetalert2": "^11.4.8",
"v-money3": "^3.24.1",
"vue": "^3.5.13",
"vue-avatar-cropper": "^6.1.1",
"vue-currency-input": "^3.0.4",
"vue-i18n": "^10.0.4",
"vue-loading-overlay": "^6.0.3",
"vue-media-upload": "^2.1.2",
"vue-currency-input": "^3.2.1",
"vue-i18n": "^11.1.3",
"vue-loading-overlay": "^6.0.6",
"vue-media-upload": "^2.2.4",
"vue-persian-datetime-picker": "^2.10.4",
"vue-router": "^4.1.6",
"vue-router": "^4.5.0",
"vue-select": "^4.0.0-beta.6",
"vue-spinner": "^1.0.4",
"vue3-apexcharts": "^1.8.0",
"vue3-easy-data-table": "^1.5.42",
"vue3-easy-data-table": "^1.5.47",
"vue3-perfect-scrollbar": "^2.0.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-tel-input": "^1.0.4",
"vue3-treeselect": "^0.1.10",
"vue3-treeview": "^0.4.1",
"vuetify": "^3.7.4"
"vue3-treeview": "^0.4.2",
"vuetify": "^3.8.2"
},
"devDependencies": {
"@types/file-saver": "^2.0.5",
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3",
"@types/file-saver": "^2.0.7",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"npm-run-all": "^4.1.5",
"sass": "^1.67.0",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
"sass": "^1.87.0",
"typescript": "^5.8.3",
"vite": "^6.3.2",
"vue-tsc": "^2.2.10"
},
"build:pwa": "vue-cli-service build && workbox generateSW workbox-config.js"
}

View file

@ -0,0 +1,303 @@
<template>
<v-menu
v-model="menu"
:close-on-content-click="false"
location="bottom"
width="400"
>
<template v-slot:activator="{ props }">
<v-text-field
v-bind="props"
:model-value="selectedAccountName"
:label="label"
:rules="rules"
readonly
hide-details
density="compact"
@click="openMenu"
>
<template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template>
</v-text-field>
</template>
<v-card>
<v-progress-linear v-if="isLoading" indeterminate color="primary" />
<v-card-text v-if="accountData.length > 0" class="pa-0">
<div class="tree-container">
<div
v-for="item in accountData"
:key="item.id"
class="tree-node"
:class="{ 'has-children': item.children && item.children.length > 0 }"
>
<div class="tree-node-content" @click="handleNodeClick(item)">
<div class="tree-node-toggle" @click.stop="toggleNode(item)">
<v-icon v-if="item.children && item.children.length > 0">
{{ item.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</div>
<div class="tree-node-icon">
<v-icon v-if="item.children && item.children.length > 0">mdi-folder</v-icon>
<v-icon v-else>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === item.id }">
{{ item.name }}
</div>
</div>
<div v-if="item.isOpen && item.children && item.children.length > 0" class="tree-children">
<div
v-for="child in item.children"
:key="child.id"
class="tree-node"
:class="{ 'has-children': child.children && child.children.length > 0 }"
>
<div class="tree-node-content" @click="handleNodeClick(child)">
<div class="tree-node-toggle" @click.stop="toggleNode(child)">
<v-icon v-if="child.children && child.children.length > 0">
{{ child.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</div>
<div class="tree-node-icon">
<v-icon v-if="child.children && child.children.length > 0">mdi-folder</v-icon>
<v-icon v-else>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === child.id }">
{{ child.name }}
</div>
</div>
<div v-if="child.isOpen && child.children && child.children.length > 0" class="tree-children">
<div
v-for="grandChild in child.children"
:key="grandChild.id"
class="tree-node"
>
<div class="tree-node-content" @click="handleNodeClick(grandChild)">
<div class="tree-node-icon">
<v-icon>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === grandChild.id }">
{{ grandChild.name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-text v-else>
در حال بارگذاری...
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue';
import axios from 'axios';
import { debounce } from 'lodash'; // برای دیبانس کردن loadChildren
const props = defineProps({
modelValue: {
type: Number,
default: null
},
label: {
type: String,
default: 'حساب'
},
rules: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'select']);
const menu = ref(false);
const accountData = ref([]);
const selectedAccount = ref(null);
const cache = ref(new Map());
const isLoading = ref(false);
const selectedAccountName = computed(() => {
return selectedAccount.value?.name || '';
});
// تابع برای رمزگشایی کاراکترهای یونیکد
const decodeUnicode = (str: string): string => {
try {
return decodeURIComponent(
str.replace(/\\u([\dA-F]{4})/gi, (match, grp) =>
String.fromCharCode(parseInt(grp, 16))
)
);
} catch (e) {
console.error('خطا در رمزگشایی یونیکد:', e);
return str;
}
};
// پردازش دادهها برای رمزگشایی نامها
const processTreeData = (items: any[]): any[] => {
return items.map(item => {
if (cache.value.has(`processed-${item.id}`)) {
return cache.value.get(`processed-${item.id}`);
}
const processedItem = {
...item,
name: decodeUnicode(item.name),
children: item.children ? processTreeData(item.children) : [],
isOpen: false
};
cache.value.set(`processed-${item.id}`, processedItem);
return processedItem;
});
};
// بارگذاری تنبل زیرشاخهها با دیبانس
const loadChildren = debounce(async (node: any) => {
if (cache.value.has(node.id)) {
node.children = cache.value.get(node.id);
return;
}
try {
const response = await axios.get(`/api/hesabdari/tables/${node.id}/children`);
if (response.data.Success) {
const children = processTreeData(response.data.data || []);
node.children = children;
cache.value.set(node.id, children);
}
} catch (error) {
console.error(`خطا در بارگذاری زیرشاخه‌های گره ${node.id}:`, error);
}
}, 300);
const toggleNode = (node: any) => {
if (node.children && node.children.length > 0) {
node.isOpen = !node.isOpen;
if (node.isOpen && (!node.children || node.children.length === 0)) {
loadChildren(node);
}
}
};
// مدیریت انتخاب آیتمها
const handleNodeClick = (node: any) => {
selectedAccount.value = node;
emit('update:modelValue', node.id);
emit('select', node);
menu.value = false;
};
// باز کردن منو
const openMenu = () => {
menu.value = true;
if (!accountData.value.length && !isLoading.value) {
fetchHesabdariTables();
}
};
// بارگذاری اولیه گرههای ریشه
const fetchHesabdariTables = async () => {
if (cache.value.has('root')) {
accountData.value = cache.value.get('root');
return;
}
isLoading.value = true;
try {
const response = await axios.get('/api/hesabdari/tables');
if (response.data.Success && response.data.data) {
accountData.value = processTreeData(response.data.data[0].children || []);
cache.value.set('root', accountData.value);
}
} catch (error) {
console.error('خطا در بارگذاری حساب‌ها:', error);
} finally {
isLoading.value = false;
}
};
// دیباگ تعداد مونتها
onMounted(() => {
fetchHesabdariTables();
});
// بررسی تغییرات در vue-router
watch(
() => props.modelValue,
() => {
console.log('modelValue تغییر کرد، احتمالاً به دلیل ناوبری');
}
);
</script>
<style scoped>
.tree-container {
max-height: 300px;
overflow-y: auto;
padding: 8px;
}
.tree-node {
margin-left: 24px;
}
.tree-node-content {
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.tree-node-content:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
}
.tree-node-toggle {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
}
.tree-node-icon {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
.tree-node-label {
flex: 1;
font-size: 0.9rem;
font-family: 'Vazir', sans-serif;
}
.tree-node-label.selected {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
.tree-children {
margin-left: 24px;
border-right: 2px solid rgba(var(--v-theme-primary), 0.1);
padding-right: 8px;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
</style>

View file

@ -7,7 +7,6 @@
v-model="showBarChart"
:label="$t('dashboard.topCommodities.chartToggle')"
color="primary"
size="small"
density="compact"
hide-details
></v-switch>

View file

@ -13,10 +13,6 @@ import faIR from 'date-fns-jalali/locale/fa-IR';
import { createPinia } from 'pinia'
const pinia = createPinia();
import CKEditor from '@ckeditor/ckeditor5-vue';
// Import translations for the Persian language.
import '@ckeditor/ckeditor5-build-classic/build/translations/fa';
// Vuetify
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
@ -156,7 +152,7 @@ app.component('v-cob', vSelect)
import Hdatepicker from "@/components/forms/Hdatepicker.vue";
import calendarLocalConfig from "@/i18n/calendarLocalConfig";
app.component('h-date-picker', Hdatepicker);
app.use(CKEditor)
app.use(Vue3PersianDatetimePicker, {
name: 'CustomDatePicker',
props: {

View file

@ -1,4 +1,27 @@
<template>
<v-toolbar color="toolbar" :title="$t('dialog.accounting_doc')">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-tooltip text="ثبت سند" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-content-save" color="success" @click="submitForm" :loading="loading"></v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="docId" text="حذف سند" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-delete" color="error" @click="deleteDialog = true" :loading="loading"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-container>
<v-form @submit.prevent="submitForm">
<v-row>
@ -17,78 +40,137 @@
</v-col>
</v-row>
<v-data-table
:headers="headers"
:items="form.rows"
class="elevation-1"
hide-default-footer
:header-props="{ class: 'custom-header' }"
>
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>ردیفهای سند</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn color="primary" @click="addRow">افزودن ردیف</v-btn>
</v-toolbar>
</template>
<v-table class="border rounded d-none d-sm-table mt-3" style="width: 100%;">
<thead>
<tr style="background-color: #0D47A1; color: white; height: 40px;">
<th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">حساب</th>
<th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">تفصیل</th>
<th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">توضیحات</th>
<th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">بدهکار</th>
<th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">بستانکار</th>
<th class="text-center" style="width: 50px; font-size: 0.8rem; padding: 0 4px;">عملیات</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in form.rows" :key="index">
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '40px' }">
<td class="text-center" style="min-width: 150px; padding: 0 4px;">
<Haccountsearch
v-model="row.ref"
:rules="[v => !!v || 'حساب الزامی است']"
@account-selected="(account) => handleAccountSelect(row, account)"
/>
</td>
<td class="text-center" style="min-width: 100px; padding: 0 4px;">
</td>
<td class="text-center" style="padding: 0 4px;">
<v-text-field
v-model="row.des"
label="توضیحات"
density="compact"
class="my-0"
style="font-size: 0.7rem;"
hide-details
></v-text-field>
</td>
<td class="text-center" style="width: 100px; padding: 0 4px;">
<v-text-field
v-model="row.bd"
label="بدهکار"
type="number"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.7rem;"
hide-details
></v-text-field>
</td>
<td class="text-center" style="width: 100px; padding: 0 4px;">
<v-text-field
v-model="row.bs"
label="بستانکار"
type="number"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.7rem;"
hide-details
></v-text-field>
</td>
<td class="text-center" style="width: 50px; padding: 0 4px;">
<v-tooltip text="حذف" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-delete" variant="text" size="x-small" color="error"
@click="removeRow(row)" style="min-width: 30px;"></v-btn>
</template>
</v-tooltip>
</td>
</tr>
</template>
<tr>
<td colspan="6" class="text-center pa-1" style="height: 40px;">
<v-btn color="primary" prepend-icon="mdi-plus" size="x-small" @click="addRow">افزودن سطر جدید</v-btn>
</td>
</tr>
</tbody>
</v-table>
<template v-slot:item.ref="{ item }">
<v-menu offset-y>
<template v-slot:activator="{ props }">
<v-text-field
v-model="item.refName"
label="حساب"
dense
readonly
v-bind="props"
:rules="[v => !!item.ref || 'حساب الزامی است']"
<!-- جدول موبایل -->
<div class="d-sm-none">
<v-card v-for="(row, index) in form.rows" :key="index" class="mb-4" variant="outlined">
<v-card-text>
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-2 font-weight-bold">ردیف:</span>
<span>{{ index + 1 }}</span>
</div>
<div class="mb-2">
<Haccountsearch
v-model="row.ref"
:rules="[v => !!v || 'حساب الزامی است']"
@account-selected="(account) => handleAccountSelect(row, account)"
/>
</div>
<div class="mb-2">
<v-text-field
v-model="row.des"
label="توضیحات"
density="compact"
class="my-0"
style="font-size: 0.8rem;"
></v-text-field>
</template>
<v-treeview
:items="hesabdariTables"
item-key="id"
item-text="name"
item-children="children"
selectable
return-object
v-model="item.selectedAccounts"
@update:active="selectAccount(item, $event)"
>
<template v-slot:label="{ item: treeItem }">
{{ treeItem.name }}
</template>
</v-treeview>
</v-menu>
</template>
<template v-slot:item.bd="{ item }">
<v-text-field
v-model="item.bd"
label="بدهکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.bs="{ item }">
<v-text-field
v-model="item.bs"
label="بستانکار"
type="number"
dense
@input="calculateTotals"
></v-text-field>
</template>
<template v-slot:item.des="{ item }">
<v-text-field v-model="item.des" label="توضیحات" dense></v-text-field>
</template>
<template v-slot:item.actions="{ item }">
<v-btn color="error" small @click="removeRow(item)">حذف</v-btn>
</template>
</v-data-table>
</div>
<div class="d-flex justify-space-between mb-2">
<div style="width: 48%;">
<v-text-field
v-model="row.bd"
label="بدهکار"
type="number"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.8rem;"
></v-text-field>
</div>
<div style="width: 48%;">
<v-text-field
v-model="row.bs"
label="بستانکار"
type="number"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.8rem;"
></v-text-field>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon="mdi-delete" variant="text" color="error" @click="removeRow(row)"></v-btn>
</v-card-actions>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" block class="mb-4" @click="addRow">افزودن ردیف جدید</v-btn>
</div>
<v-row class="mt-4">
<v-col cols="6">
@ -110,21 +192,41 @@
</v-row>
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert>
<v-btn type="submit" color="success" class="mt-4" :disabled="totalBd !== totalBs || !form.date">
ثبت سند
</v-btn>
</v-form>
</v-container>
<!-- دیالوگ تأیید حذف -->
<v-dialog v-model="deleteDialog" max-width="400">
<v-card>
<v-card-title class="text-h5">
حذف سند
</v-card-title>
<v-card-text>
آیا مطمئن هستید که میخواهید این سند را حذف کنید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="deleteDialog = false">
انصراف
</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete" :loading="loading">
حذف
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import axios from 'axios';
import moment from 'jalali-moment';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import Haccountsearch from '@/components/forms/Haccountsearch.vue';
export default {
components: {
Hdatepicker,
Haccountsearch
},
props: {
docId: {
@ -135,7 +237,7 @@ export default {
data() {
return {
form: {
date: '', // تاریخ به فرمت ISO (مثلاً 2025-03-24)
date: '',
des: '',
rows: [
{ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] },
@ -145,13 +247,8 @@ export default {
totalBd: 0,
totalBs: 0,
error: null,
headers: [
{ text: 'حساب', value: 'ref' },
{ text: 'بدهکار', value: 'bd' },
{ text: 'بستانکار', value: 'bs' },
{ text: 'توضیحات', value: 'des' },
{ text: 'عملیات', value: 'actions', sortable: false },
],
deleteDialog: false,
loading: false,
};
},
mounted() {
@ -173,7 +270,7 @@ export default {
async fetchDoc() {
try {
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`);
const serverDate = response.data.data.date; // فرض: تاریخ شمسی از سرور
const serverDate = response.data.data.date;
this.form.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY-MM-DD');
this.form.des = response.data.data.des || '';
this.form.rows = response.data.data.rows.map(row => ({
@ -219,7 +316,7 @@ export default {
}
const payload = {
date: moment(this.form.date, 'YYYY-MM-DD').locale('fa').format('YYYY/MM/DD'), // ارسال به فرمت شمسی
date: moment(this.form.date, 'YYYY-MM-DD').locale('fa').format('YYYY/MM/DD'),
des: this.form.des,
rows: this.form.rows.map(row => ({
ref: row.ref,
@ -230,6 +327,7 @@ export default {
};
try {
this.loading = true;
if (this.docId) {
await axios.put(`/api/hesabdari/doc/${this.docId}`, payload);
this.$emit('saved', 'سند با موفقیت ویرایش شد');
@ -239,6 +337,21 @@ export default {
}
} catch (error) {
this.error = error.response?.data?.message || 'خطا در ثبت سند';
} finally {
this.loading = false;
}
},
async confirmDelete() {
try {
this.loading = true;
await axios.delete(`/api/hesabdari/doc/${this.docId}`);
this.$router.push('/acc/accounting/list');
} catch (error) {
this.error = 'خطا در حذف سند';
console.error(error);
} finally {
this.loading = false;
this.deleteDialog = false;
}
},
},

View file

@ -1,42 +1,28 @@
<script lang="ts">
import { defineComponent } from 'vue'
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import axios from "axios";
import Swal from "sweetalert2";
import { defineComponent } from 'vue';
import axios from 'axios';
import Swal from 'sweetalert2';
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css';
export default defineComponent({
name: "mod",
name: 'mod',
components: { Loading },
data: () => {
return {
loading: true,
id: '',
version: '',
body: '',
editor: ClassicEditor,
editorConfig: {
language: 'fa',
fontFamily: {
options: [
'default',
'vazir', 'sans-serif',
'Ubuntu Mono, Courier New, Courier, monospace'
]
},
}
}
},
data: () => ({
loading: true,
id: '',
version: '',
body: '',
}),
mounted() {
this.id = this.$route.params.id;
if (this.id != 0) {
if (this.id !== '0') {
axios.post('/api/admin/reportchange/get/' + this.id).then((response) => {
this.version = response.data.version;
this.body = response.data.body;
this.loading = false;
});
}
else {
} else {
this.loading = false;
}
},
@ -48,29 +34,30 @@ export default defineComponent({
icon: 'error',
confirmButtonText: 'قبول',
});
}
else {
} else {
this.loading = true;
axios.post('/api/admin/reportchange/mod/' + this.id, {
id: this.id,
version: this.version,
body: this.body
}).then((response) => {
if (response.data.result == 1) {
this.loading = false;
Swal.fire({
text: 'گزارش ثبت شد',
icon: 'success',
confirmButtonText: 'قبول',
}).then((res) => {
this.$router.push('/profile/manager/changes/list');
})
}
})
axios
.post('/api/admin/reportchange/mod/' + this.id, {
id: this.id,
version: this.version,
body: this.body,
})
.then((response) => {
if (response.data.result === 1) {
this.loading = false;
Swal.fire({
text: 'گزارش ثبت شد',
icon: 'success',
confirmButtonText: 'قبول',
}).then(() => {
this.$router.push('/profile/manager/changes/list');
});
}
});
}
}
}
})
},
},
});
</script>
<template>
@ -82,20 +69,38 @@ export default defineComponent({
<v-card-text class="pa-2">
<v-row>
<v-col cols="12" sm="12" md="6">
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.version')" v-model="version" type="text"
<v-text-field
hide-details="auto"
:label="$t('pages.manager.version')"
v-model="version"
type="text"
prepend-inner-icon="mdi-power-socket-uk"
:rules="[() => version.length > 0 || $t('validator.required')]"></v-text-field>
:rules="[() => version.length > 0 || $t('validator.required')]"
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="12">
<h3 class="mb-2">{{ $t('app.body') }}</h3>
<ckeditor :editor="editor" v-model="body" :config="editorConfig"
:rules="[() => version.length > 0 || $t('validator.required')]"></ckeditor>
<v-textarea
v-model="body"
:rules="[() => body.length > 0 || $t('validator.required')]"
auto-grow
variant="outlined"
class="font-weight-regular"
style="font-family: 'Vazirmatn FD', sans-serif;"
></v-textarea>
</v-col>
<v-col cols="12" sm="12" md="12">
<v-btn type="submit" @click="submit()" color="primary" prepend-icon="mdi-content-save" :loading="loading"
:title="$t('dialog.save')">
<v-btn
type="submit"
@click="submit()"
color="primary"
prepend-icon="mdi-content-save"
:loading="loading"
:title="$t('dialog.save')"
>
{{ $t('dialog.save') }}
</v-btn>
</v-btn>
</v-col>
</v-row>
</v-card-text>
@ -103,4 +108,12 @@ export default defineComponent({
</v-container>
</template>
<style scoped></style>
<style scoped>
.v-textarea {
font-family: 'Vazirmatn FD', sans-serif;
font-size: 14px;
line-height: 1.8;
text-align: right;
direction: rtl;
}
</style>

View file

@ -1,8 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node",
"target": "esnext",
"types": ["node"],
}
}
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
},
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"]
}

View file

@ -1,5 +1,5 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
@ -7,12 +7,16 @@
"@/*": ["./src/*"]
},
"allowJs": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
}