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", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"php": ">=8.1", "php": ">=8.2",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"ext-iconv": "*", "ext-iconv": "*",
"doctrine/annotations": "^1.0", "doctrine/annotations": "^2.0",
"doctrine/dbal": "^3.9", "doctrine/dbal": "^4.2",
"doctrine/doctrine-bundle": "^2.8", "doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^2.14", "doctrine/orm": "^3.2",
"dompdf/dompdf": "^2.0", "dompdf/dompdf": "^3.0",
"melipayamak/php": "1.0.0", "melipayamak/php": "^1.0",
"mpdf/mpdf": "^8.2", "mpdf/mpdf": "^8.2",
"nelmio/api-doc-bundle": "^4.34", "nelmio/api-doc-bundle": "^4.35",
"nelmio/cors-bundle": "^2.5", "nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.3", "phpdocumentor/reflection-docblock": "^5.4",
"phpoffice/phpspreadsheet": "^1.29", "phpoffice/phpspreadsheet": "^2.3",
"phpstan/phpdoc-parser": "^1.16", "phpstan/phpdoc-parser": "^1.33",
"ramsey/uuid": "^4.7", "ramsey/uuid": "^4.7",
"symfony/apache-pack": "^1.0", "symfony/apache-pack": "^1.0",
"symfony/asset": "7.1.*", "symfony/asset": "7.2.*",
"symfony/console": "7.1.*", "symfony/console": "7.2.*",
"symfony/doctrine-messenger": "7.1.*", "symfony/doctrine-messenger": "7.2.*",
"symfony/dotenv": "7.1.*", "symfony/dotenv": "7.2.*",
"symfony/expression-language": "7.1.*", "symfony/expression-language": "7.2.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "7.1.*", "symfony/form": "7.2.*",
"symfony/framework-bundle": "7.1.*", "symfony/framework-bundle": "7.2.*",
"symfony/http-client": "7.1.*", "symfony/http-client": "7.2.*",
"symfony/lock": "7.1.*", "symfony/lock": "7.2.*",
"symfony/mailer": "7.1.*", "symfony/mailer": "7.2.*",
"symfony/mime": "7.1.*", "symfony/mime": "7.2.*",
"symfony/monolog-bundle": "^3.0", "symfony/monolog-bundle": "^3.10",
"symfony/notifier": "7.1.*", "symfony/notifier": "7.2.*",
"symfony/process": "7.1.*", "symfony/process": "7.2.*",
"symfony/property-access": "7.1.*", "symfony/property-access": "7.2.*",
"symfony/property-info": "7.1.*", "symfony/property-info": "7.2.*",
"symfony/runtime": "7.1.*", "symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.1.*", "symfony/security-bundle": "7.2.*",
"symfony/serializer": "7.1.*", "symfony/serializer": "7.2.*",
"symfony/string": "7.1.*", "symfony/string": "7.2.*",
"symfony/translation": "7.1.*", "symfony/translation": "7.2.*",
"symfony/twig-bundle": "7.1.*", "symfony/twig-bundle": "7.2.*",
"symfony/validator": "7.1.*", "symfony/validator": "7.2.*",
"symfony/web-link": "7.1.*", "symfony/web-link": "7.2.*",
"symfony/yaml": "7.1.*", "symfony/yaml": "7.2.*",
"symfonycasts/verify-email-bundle": "^1.13", "symfonycasts/verify-email-bundle": "^1.17",
"tecnickcom/tcpdf": "^6.6", "tecnickcom/tcpdf": "^6.7",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^3.14",
"twig/twig": "^2.12|^3.0" "twig/twig": "^3.14"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@ -99,19 +99,19 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": true, "allow-contrib": true,
"require": "7.1.*", "require": "7.2.*",
"docker": true "docker": true
}, },
"public-dir": "../public_html" "public-dir": "../public_html"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^11.4",
"symfony/browser-kit": "7.1.*", "symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.1.*", "symfony/css-selector": "7.2.*",
"symfony/debug-bundle": "7.1.*", "symfony/debug-bundle": "7.2.*",
"symfony/maker-bundle": "^1.48", "symfony/maker-bundle": "^1.62",
"symfony/phpunit-bridge": "^7.1", "symfony/phpunit-bridge": "^7.2",
"symfony/stopwatch": "7.1.*", "symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.1.*" "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\Salary;
use App\Entity\Person; use App\Entity\Person;
use App\Service\Log; use App\Service\Log;
use Doctrine\Common\Collections\ArrayCollection;
class CostController extends AbstractController class CostController extends AbstractController
{ {
@ -138,7 +139,10 @@ class CostController extends AbstractController
->andWhere('r.bd != 0') ->andWhere('r.bd != 0')
->groupBy('t.id, t.name') ->groupBy('t.id, t.name')
->orderBy('total_cost', 'DESC') ->orderBy('total_cost', 'DESC')
->setParameters($parameters); ->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'cost')
->setParameter('year', $acc['year']);
// اعمال فیلتر تاریخ فقط برای امروز و ماه // اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') { if ($period === 'today') {

View file

@ -1134,4 +1134,36 @@ class HesabdariController extends AbstractController
return $this->json($extractor->operationSuccess($pdfPid)); 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\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Doctrine\Common\Collections\ArrayCollection;
class IncomeController extends AbstractController class IncomeController extends AbstractController
{ {
@ -111,14 +112,6 @@ class IncomeController extends AbstractController
$today = $jdate->jdate('Y/m/d', time()); $today = $jdate->jdate('Y/m/d', time());
$monthStart = $jdate->jdate('Y/m/01', time()); $monthStart = $jdate->jdate('Y/m/01', time());
// پارامترهای پایه
$parameters = [
'bid' => $acc['bid'],
'money' => $acc['money'],
'type' => 'income',
'year' => $acc['year'],
];
// کوئری پایه // کوئری پایه
$qb = $entityManager->createQueryBuilder() $qb = $entityManager->createQueryBuilder()
->select('t.name AS center_name, SUM(COALESCE(r.bs, 0)) AS total_income') ->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 صفر نیست ->andWhere('r.bs != 0') // فقط ردیف‌هایی که bs صفر نیست
->groupBy('t.id, t.name') ->groupBy('t.id, t.name')
->orderBy('total_income', 'DESC') ->orderBy('total_income', 'DESC')
->setParameters($parameters); ->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money'])
->setParameter('type', 'income')
->setParameter('year', $acc['year']);
// اعمال فیلتر تاریخ فقط برای امروز و ماه // اعمال فیلتر تاریخ فقط برای امروز و ماه
if ($period === 'today') { 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\Lexer;
use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
class Cast extends FunctionNode class Cast extends FunctionNode
{ {
private $expression; private $expression;
public function parse(Parser $parser) public function parse(Parser $parser): void
{ {
$parser->match(Lexer::T_IDENTIFIER); // CAST $parser->match(TokenType::T_IDENTIFIER); // CAST
$parser->match(Lexer::T_OPEN_PARENTHESIS); $parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->expression = $parser->ArithmeticExpression(); $this->expression = $parser->ArithmeticExpression();
$parser->match(Lexer::T_AS); $parser->match(TokenType::T_AS);
$parser->match(Lexer::T_IDENTIFIER); // INTEGER یا هر نوع دیگه $parser->match(TokenType::T_IDENTIFIER); // INTEGER یا هر نوع دیگه
$parser->match(Lexer::T_CLOSE_PARENTHESIS); $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)'; return 'CAST(' . $sqlWalker->walkArithmeticPrimary($this->expression) . ' AS SIGNED)';
} }
} }

View file

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

View file

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

View file

@ -122,6 +122,18 @@
"./.env" "./.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": { "symfony/framework-bundle": {
"version": "6.2", "version": "6.2",
"recipe": { "recipe": {

View file

@ -1,6 +1,6 @@
{ {
"name": "hesabix", "name": "hesabix",
"version": "0.45.0", "version": "0.48.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -10,68 +10,62 @@
"type-check": "vue-tsc --noEmit" "type-check": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@chenfengyuan/vue-countdown": "^2.1.2", "@chenfengyuan/vue-countdown": "^2.1.3",
"@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",
"@date-io/date-fns-jalali": "^3.2.0", "@date-io/date-fns-jalali": "^3.2.0",
"@mdi/font": "^7.4.47", "@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/extension-text-align": "^2.11.7",
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7", "@tiptap/starter-kit": "^2.11.7",
"@tiptap/vue-3": "^2.11.7", "@tiptap/vue-3": "^2.11.7",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^13.1.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"apexcharts": "^4.4.0", "apexcharts": "^4.6.0",
"axios": "^1.2.3", "axios": "^1.8.4",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-jalali": "^3.2.0-0", "date-fns-jalali": "^3.2.0-0",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jalali-moment": "^3.3.11", "jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.10.44", "libphonenumber-js": "^1.12.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maska": "^3.0.4", "maska": "^3.1.1",
"maz-ui": "^3.11.4", "maz-ui": "^3.50.1",
"pinia": "^2.2.6", "pinia": "^3.0.2",
"sweetalert2": "^11.6.13", "sweetalert2": "^11.4.8",
"v-money3": "^3.24.0", "v-money3": "^3.24.1",
"v-skeleton-loader": "^0.1.9",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-avatar-cropper": "^6.1.1", "vue-avatar-cropper": "^6.1.1",
"vue-currency-input": "^3.0.4", "vue-currency-input": "^3.2.1",
"vue-i18n": "^10.0.4", "vue-i18n": "^11.1.3",
"vue-loading-overlay": "^6.0.3", "vue-loading-overlay": "^6.0.6",
"vue-media-upload": "^2.1.2", "vue-media-upload": "^2.2.4",
"vue-persian-datetime-picker": "^2.10.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-select": "^4.0.0-beta.6",
"vue-spinner": "^1.0.4", "vue-spinner": "^1.0.4",
"vue3-apexcharts": "^1.8.0", "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-perfect-scrollbar": "^2.0.0",
"vue3-persian-datetime-picker": "^1.2.2", "vue3-persian-datetime-picker": "^1.2.2",
"vue3-tel-input": "^1.0.4", "vue3-tel-input": "^1.0.4",
"vue3-treeselect": "^0.1.10", "vue3-treeselect": "^0.1.10",
"vue3-treeview": "^0.4.1", "vue3-treeview": "^0.4.2",
"vuetify": "^3.7.4" "vuetify": "^3.8.2"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.7",
"@types/node": "^18.11.12", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.7.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.67.0", "sass": "^1.87.0",
"typescript": "~4.7.4", "typescript": "^5.8.3",
"vite": "^4.0.0", "vite": "^6.3.2",
"vue-tsc": "^1.0.12" "vue-tsc": "^2.2.10"
}, },
"build:pwa": "vue-cli-service build && workbox generateSW workbox-config.js" "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" v-model="showBarChart"
:label="$t('dashboard.topCommodities.chartToggle')" :label="$t('dashboard.topCommodities.chartToggle')"
color="primary" color="primary"
size="small"
density="compact" density="compact"
hide-details hide-details
></v-switch> ></v-switch>

View file

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

View file

@ -1,4 +1,27 @@
<template> <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-container>
<v-form @submit.prevent="submitForm"> <v-form @submit.prevent="submitForm">
<v-row> <v-row>
@ -17,78 +40,137 @@
</v-col> </v-col>
</v-row> </v-row>
<v-data-table <v-table class="border rounded d-none d-sm-table mt-3" style="width: 100%;">
:headers="headers" <thead>
:items="form.rows" <tr style="background-color: #0D47A1; color: white; height: 40px;">
class="elevation-1" <th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">حساب</th>
hide-default-footer <th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">تفصیل</th>
:header-props="{ class: 'custom-header' }" <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>
<template v-slot:top> <th class="text-center" style="font-size: 0.8rem; padding: 0 4px;">بستانکار</th>
<v-toolbar flat> <th class="text-center" style="width: 50px; font-size: 0.8rem; padding: 0 4px;">عملیات</th>
<v-toolbar-title>ردیفهای سند</v-toolbar-title> </tr>
<v-spacer></v-spacer> </thead>
<v-btn color="primary" @click="addRow">افزودن ردیف</v-btn> <tbody>
</v-toolbar> <template v-for="(row, index) in form.rows" :key="index">
</template> <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> <div class="d-sm-none">
<template v-slot:activator="{ props }"> <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-text-field
v-model="item.refName" v-model="row.des"
label="حساب" label="توضیحات"
dense density="compact"
readonly class="my-0"
v-bind="props" style="font-size: 0.8rem;"
:rules="[v => !!item.ref || 'حساب الزامی است']"
></v-text-field> ></v-text-field>
</template> </div>
<v-treeview <div class="d-flex justify-space-between mb-2">
:items="hesabdariTables" <div style="width: 48%;">
item-key="id" <v-text-field
item-text="name" v-model="row.bd"
item-children="children" label="بدهکار"
selectable type="number"
return-object density="compact"
v-model="item.selectedAccounts" @input="calculateTotals"
@update:active="selectAccount(item, $event)" class="my-0"
> style="font-size: 0.8rem;"
<template v-slot:label="{ item: treeItem }"> ></v-text-field>
{{ treeItem.name }} </div>
</template> <div style="width: 48%;">
</v-treeview> <v-text-field
</v-menu> v-model="row.bs"
</template> label="بستانکار"
type="number"
<template v-slot:item.bd="{ item }"> density="compact"
<v-text-field @input="calculateTotals"
v-model="item.bd" class="my-0"
label="بدهکار" style="font-size: 0.8rem;"
type="number" ></v-text-field>
dense </div>
@input="calculateTotals" </div>
></v-text-field> </v-card-text>
</template> <v-card-actions>
<v-spacer></v-spacer>
<template v-slot:item.bs="{ item }"> <v-btn icon="mdi-delete" variant="text" color="error" @click="removeRow(row)"></v-btn>
<v-text-field </v-card-actions>
v-model="item.bs" </v-card>
label="بستانکار" <v-btn color="primary" prepend-icon="mdi-plus" block class="mb-4" @click="addRow">افزودن ردیف جدید</v-btn>
type="number" </div>
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>
<v-row class="mt-4"> <v-row class="mt-4">
<v-col cols="6"> <v-col cols="6">
@ -110,21 +192,41 @@
</v-row> </v-row>
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert> <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-form>
</v-container> </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> </template>
<script> <script>
import axios from 'axios'; import axios from 'axios';
import moment from 'jalali-moment'; import moment from 'jalali-moment';
import Hdatepicker from '@/components/forms/Hdatepicker.vue'; import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import Haccountsearch from '@/components/forms/Haccountsearch.vue';
export default { export default {
components: { components: {
Hdatepicker, Hdatepicker,
Haccountsearch
}, },
props: { props: {
docId: { docId: {
@ -135,7 +237,7 @@ export default {
data() { data() {
return { return {
form: { form: {
date: '', // تاریخ به فرمت ISO (مثلاً 2025-03-24) date: '',
des: '', des: '',
rows: [ rows: [
{ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] }, { ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] },
@ -145,13 +247,8 @@ export default {
totalBd: 0, totalBd: 0,
totalBs: 0, totalBs: 0,
error: null, error: null,
headers: [ deleteDialog: false,
{ text: 'حساب', value: 'ref' }, loading: false,
{ text: 'بدهکار', value: 'bd' },
{ text: 'بستانکار', value: 'bs' },
{ text: 'توضیحات', value: 'des' },
{ text: 'عملیات', value: 'actions', sortable: false },
],
}; };
}, },
mounted() { mounted() {
@ -173,7 +270,7 @@ export default {
async fetchDoc() { async fetchDoc() {
try { try {
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`); 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.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY-MM-DD');
this.form.des = response.data.data.des || ''; this.form.des = response.data.data.des || '';
this.form.rows = response.data.data.rows.map(row => ({ this.form.rows = response.data.data.rows.map(row => ({
@ -219,7 +316,7 @@ export default {
} }
const payload = { 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, des: this.form.des,
rows: this.form.rows.map(row => ({ rows: this.form.rows.map(row => ({
ref: row.ref, ref: row.ref,
@ -230,6 +327,7 @@ export default {
}; };
try { try {
this.loading = true;
if (this.docId) { if (this.docId) {
await axios.put(`/api/hesabdari/doc/${this.docId}`, payload); await axios.put(`/api/hesabdari/doc/${this.docId}`, payload);
this.$emit('saved', 'سند با موفقیت ویرایش شد'); this.$emit('saved', 'سند با موفقیت ویرایش شد');
@ -239,6 +337,21 @@ export default {
} }
} catch (error) { } catch (error) {
this.error = error.response?.data?.message || 'خطا در ثبت سند'; 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"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import axios from 'axios';
import axios from "axios"; import Swal from 'sweetalert2';
import Swal from "sweetalert2";
import Loading from 'vue-loading-overlay'; import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/css/index.css'; import 'vue-loading-overlay/dist/css/index.css';
export default defineComponent({ export default defineComponent({
name: "mod", name: 'mod',
components: { Loading }, components: { Loading },
data: () => { data: () => ({
return { loading: true,
loading: true, id: '',
id: '', version: '',
version: '', body: '',
body: '', }),
editor: ClassicEditor,
editorConfig: {
language: 'fa',
fontFamily: {
options: [
'default',
'vazir', 'sans-serif',
'Ubuntu Mono, Courier New, Courier, monospace'
]
},
}
}
},
mounted() { mounted() {
this.id = this.$route.params.id; this.id = this.$route.params.id;
if (this.id != 0) { if (this.id !== '0') {
axios.post('/api/admin/reportchange/get/' + this.id).then((response) => { axios.post('/api/admin/reportchange/get/' + this.id).then((response) => {
this.version = response.data.version; this.version = response.data.version;
this.body = response.data.body; this.body = response.data.body;
this.loading = false; this.loading = false;
}); });
} } else {
else {
this.loading = false; this.loading = false;
} }
}, },
@ -48,29 +34,30 @@ export default defineComponent({
icon: 'error', icon: 'error',
confirmButtonText: 'قبول', confirmButtonText: 'قبول',
}); });
} } else {
else {
this.loading = true; this.loading = true;
axios.post('/api/admin/reportchange/mod/' + this.id, { axios
id: this.id, .post('/api/admin/reportchange/mod/' + this.id, {
version: this.version, id: this.id,
body: this.body version: this.version,
}).then((response) => { body: this.body,
if (response.data.result == 1) { })
this.loading = false; .then((response) => {
Swal.fire({ if (response.data.result === 1) {
text: 'گزارش ثبت شد', this.loading = false;
icon: 'success', Swal.fire({
confirmButtonText: 'قبول', text: 'گزارش ثبت شد',
}).then((res) => { icon: 'success',
this.$router.push('/profile/manager/changes/list'); confirmButtonText: 'قبول',
}) }).then(() => {
} this.$router.push('/profile/manager/changes/list');
}) });
}
});
} }
} },
} },
}) });
</script> </script>
<template> <template>
@ -82,20 +69,38 @@ export default defineComponent({
<v-card-text class="pa-2"> <v-card-text class="pa-2">
<v-row> <v-row>
<v-col cols="12" sm="12" md="6"> <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" 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>
<v-col cols="12" sm="12" md="12"> <v-col cols="12" sm="12" md="12">
<h3 class="mb-2">{{ $t('app.body') }}</h3> <h3 class="mb-2">{{ $t('app.body') }}</h3>
<ckeditor :editor="editor" v-model="body" :config="editorConfig" <v-textarea
:rules="[() => version.length > 0 || $t('validator.required')]"></ckeditor> 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>
<v-col cols="12" sm="12" md="12"> <v-col cols="12" sm="12" md="12">
<v-btn type="submit" @click="submit()" color="primary" prepend-icon="mdi-content-save" :loading="loading" <v-btn
:title="$t('dialog.save')"> type="submit"
@click="submit()"
color="primary"
prepend-icon="mdi-content-save"
:loading="loading"
:title="$t('dialog.save')"
>
{{ $t('dialog.save') }} {{ $t('dialog.save') }}
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
@ -103,4 +108,12 @@ export default defineComponent({
</v-container> </v-container>
</template> </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": { "compilerOptions": {
"composite": true, "composite": true,
"module": "esnext",
"moduleResolution": "node",
"target": "esnext",
"types": ["node"], "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"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
@ -7,9 +7,13 @@
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"allowJs": true, "allowJs": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["vite/client"]
}, },
"references": [ "references": [
{ {
"path": "./tsconfig.config.json" "path": "./tsconfig.config.json"