bug fix and add some new options

This commit is contained in:
Hesabix 2025-06-08 01:42:08 +00:00
parent 914c01ed44
commit 194bc613d3
17 changed files with 1721 additions and 866 deletions

View file

@ -14,6 +14,12 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Writer\Exception;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class CashdeskController extends AbstractController
{
@ -268,4 +274,132 @@ class CashdeskController extends AbstractController
'total' => count($transactions)
]);
}
/**
* @throws Exception
*/
#[Route('/api/cashdesk/card/list/excel', name: 'app_cashdesk_card_list_excel')]
public function app_cashdesk_card_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|StreamedResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year' => $acc['year']
]);
if ($prs) {
$transactions[] = $prs;
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
[
'شماره تراکنش',
'تاریخ',
'توضیحات',
'شرح سند',
'تفضیل',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getDoc()->getDes(),
$transaction->getRef()->getName(),
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear()->getlabel()
];
}
$activeWorksheet->fromArray($arrayEntity, null, 'A1');
$activeWorksheet->setRightToLeft(true);
$writer = new Xlsx($spreadsheet);
$fileName = 'کارت حساب صندوق ' . $cashdesk->getName() . '.xlsx';
$filePath = __DIR__ . '/../../var/' . $fileName;
$writer->save($filePath);
$response = new BinaryFileResponse($filePath);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$fileName
);
return $response;
}
#[Route('/api/cashdesk/card/list/print', name: 'app_cashdesk_card_list_print')]
public function app_cashdesk_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$cashdesk)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'cashdesk' => $cashdesk,
'year'=>$acc['year']
]);
if ($prs) {
$transactions[] = $prs;
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/cashdesk_card.html.twig', [
'page_title' => 'کارت حساب' . ' ' . $cashdesk->getName(),
'bid' => $acc['bid'],
'items' => $transactions,
'cashdesk' => $cashdesk
])
);
return $this->json(['id' => $pid]);
}
}

View file

@ -33,6 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\HesabdariTableRepository;
class HesabdariController extends AbstractController
{
@ -179,96 +180,192 @@ class HesabdariController extends AbstractController
]);
}
#[Route('/api/accounting/search', name: 'app_accounting_search')]
public function app_accounting_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('type', $params))
$this->createNotFoundException();
$roll = '';
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'cost')
$roll = 'cost';
elseif ($params['type'] == 'income')
$roll = 'income';
elseif ($params['type'] == 'buy')
$roll = 'buy';
elseif ($params['type'] == 'rfbuy')
$roll = 'plugAccproRfbuy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'sell')
$roll = 'sell';
elseif ($params['type'] == 'rfsell')
$roll = 'plugAccproRfsell';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$this->createNotFoundException();
$acc = $access->hasRole($roll);
if (!$acc)
#[Route('/api/accounting/search', name: 'app_hesabdari_search', methods: ['POST'])]
public function search(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
HesabdariTableRepository $hesabdariTableRepository,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('acc');
if (!$acc) {
throw $this->createAccessDeniedException();
if ($params['type'] == 'all') {
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'money' => $acc['money']
], [
'id' => 'DESC'
]);
} else {
$data = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'type' => $params['type'],
'money' => $acc['money']
], [
'id' => 'DESC'
]);
}
$params = json_decode($request->getContent(), true) ?? [];
// Input parameters
$filters = $params['filters'] ?? [];
$pagination = $params['pagination'] ?? ['page' => 1, 'limit' => 10];
$sort = $params['sort'] ?? ['sortBy' => 'id', 'sortDesc' => true];
$type = $params['type'] ?? 'all';
// Set pagination parameters
$page = max(1, $pagination['page'] ?? 1);
$limit = max(1, min(100, $pagination['limit'] ?? 10));
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.hesabdariRows', 'r')
->leftJoin('r.ref', 't')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// Add type filter if not 'all'
if ($type !== 'all') {
$queryBuilder->andWhere('d.type = :type')
->setParameter('type', $type);
}
// Apply filters
if (!empty($filters)) {
// Text search
if (isset($filters['search'])) {
$searchValue = is_array($filters['search']) ? $filters['search']['value'] : $filters['search'];
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
't.name LIKE :search',
't.code LIKE :search'
)
)
->setParameter('search', "%{$searchValue}%");
}
// Account filter
if (isset($filters['account'])) {
$accountCodes = $hesabdariTableRepository->findAllSubAccountCodes($filters['account'], $acc['bid']->getId());
if (!empty($accountCodes)) {
$queryBuilder->andWhere('t.code IN (:accountCodes)')
->setParameter('accountCodes', $accountCodes);
} else {
$queryBuilder->andWhere('1 = 0');
}
}
// Time filter
if (isset($filters['timeFilter'])) {
$today = $jdate->jdate('Y/m/d', time());
switch ($filters['timeFilter']) {
case 'today':
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
break;
case 'week':
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
break;
case 'month':
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
break;
case 'custom':
if (isset($filters['date']) && isset($filters['date']['from']) && isset($filters['date']['to'])) {
// تبدیل تاریخ‌های شمسی به میلادی
$fromDate = $filters['date']['from'];
$toDate = $filters['date']['to'];
// اطمینان از فرمت صحیح تاریخ‌ها
if (strpos($fromDate, '/') !== false && strpos($toDate, '/') !== false) {
$queryBuilder->andWhere('d.date BETWEEN :dateFrom AND :dateTo')
->setParameter('dateFrom', $fromDate)
->setParameter('dateTo', $toDate);
}
}
break;
}
}
}
// Apply sorting
$sortField = is_array($sort['sortBy']) ? ($sort['sortBy']['key'] ?? 'id') : ($sort['sortBy'] ?? 'id');
$sortDirection = ($sort['sortDesc'] ?? true) ? 'DESC' : 'ASC';
$queryBuilder->orderBy("d.$sortField", $sortDirection);
// Calculate total items
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
// Apply pagination
$queryBuilder->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDateSubmit(),
'date' => $item->getDate(),
'type' => $item->getType(),
'code' => $item->getCode(),
'des' => $item->getDes(),
'amount' => $item->getAmount(),
'submitter' => $item->getSubmitter()->getFullName(),
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
];
if ($params['type'] == 'rfsell' || $params['type'] == 'rfbuy' || $params['type'] == 'buy' || $params['type'] == 'sell') {
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($item, 'person');
$temp['person'] = '';
if ($mainRow)
$temp['person'] = Explore::ExplorePerson($mainRow->getPerson());
// Get related person info if applicable
if (in_array($doc['type'], ['rfsell', 'rfbuy', 'buy', 'sell'])) {
$personInfo = $entityManager->createQueryBuilder()
->select('p.id, p.nikename, p.code')
->from('App\Entity\HesabdariRow', 'r')
->join('r.person', 'p')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $personInfo ? [
'id' => $personInfo['id'],
'nikename' => $personInfo['nikename'],
'code' => $personInfo['code'],
] : null;
}
$temp['label'] = null;
if ($item->getInvoiceLabel()) {
$temp['label'] = [
'code' => $item->getInvoiceLabel()->getCode(),
'label' => $item->getInvoiceLabel()->getLabel()
];
}
//get status of doc
$temp['status'] = 'تسویه نشده';
$pays = 0;
foreach ($item->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
}
if ($item->getAmount() <= $pays)
$temp['status'] = 'تسویه شده';
// Get payment status
$pays = $entityManager->createQueryBuilder()
->select('SUM(rd.amount) as total_pays')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.relatedDocs', 'rd')
->where('d.id = :docId')
->setParameter('docId', $doc['id'])
->getQuery()
->getSingleScalarResult();
$dataTemp[] = $temp;
$item['status'] = ($pays && $pays >= $doc['amount']) ? 'تسویه شده' : 'تسویه نشده';
$dataTemp[] = $item;
}
return $this->json($dataTemp);
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'limit' => $limit,
]);
}
/**

View file

@ -943,15 +943,20 @@ class PersonsController extends AbstractController
], $acc['money']);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $param['id'],
'bid' => $acc['bid'],
'person' => $person,
'year' => $acc['year'],
], $acc['money']);
if (count($prs) != 0) {
$transactions[] = $prs[0];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $id,
'bid' => $acc['bid'],
'person' => $person,
'year' => $acc['year'],
], $acc['money']);
if (count($prs) != 0) {
$transactions[] = $prs[0];
}
}
}
}
}
@ -1011,15 +1016,20 @@ class PersonsController extends AbstractController
], $acc['money']);
} else {
$transactions = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $param['id'],
'bid' => $acc['bid'],
'person' => $person,
'year' => $acc['year'],
], $acc['money']);
if (count($prs) != 0) {
$transactions[] = $prs[0];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$prs = $entityManager->getRepository(HesabdariRow::class)->findByJoinMoney([
'id' => $id,
'bid' => $acc['bid'],
'person' => $person,
'year' => $acc['year'],
], $acc['money']);
if (count($prs) != 0) {
$transactions[] = $prs[0];
}
}
}
}
}

View file

@ -0,0 +1,116 @@
{% extends "pdf/base.html.twig" %}
{% block body %}
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">کارت حساب صندوق</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="">
<p>
<b>نام:
</b>
{{ cashdesk.name }}
</p>
</td>
<td class="center">
<p>
<b>
کد حسابداری:
</b>
{{ cashdesk.code }}
</p>
</td>
<td class="center">
<p>
<b>شرح:
</b>
{{ cashdesk.des }}
</p>
</td>
</tr>
</tbody>
</table>
</div>
<div style="width:100%;margin-top:5px;text-align:center;">
<table style="width:100%;">
<tbody>
<tr style="text-align: center; background-color: grey; text-color: white">
<td style="width: 35px;">ردیف</td>
<td class="center item">فاکتور/سند</td>
<td class="center item">تاریخ</td>
<td class="center item">توضیحات</td>
<td class="center item">شرح سند</td>
<td class="center item">تفضیل</td>
<td class="center item">واریز</td>
<td class="center item">برداشت</td>
<td class="center item">سال مالی</td>
</tr>
{% set sumBs = 0 %}
{% set sumBd = 0 %}
{% for item in items %}
{% set sumBs = sumBs + item.bs %}
{% set sumBd = sumBd + item.bd %}
<tr class="stimol">
<td class="center item">{{ loop.index }}</td>
<td class="center item">{{ item.doc.code }}</td>
<td class="center item">{{ item.doc.date }}</td>
<td class="center item">{{ item.des }}</td>
<td class="center item">{{ item.doc.des }}</td>
<td class="center item">{{ item.ref.name }}</td>
<td class="center item">{{ item.bd | number_format }}</td>
<td class="center item">{{ item.bs | number_format }}</td>
<td class="center item">{{ item.year.label }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">وضعیت حساب</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="center">
<p>
<b>جمع برداشت:
</b>
{{ sumBs | number_format }}
</p>
</td>
<td class="center">
<p>
<b>جمع واریز:
</b>
{{ sumBd | number_format }}
</p>
</td>
<td class="center">
<p>
<b>تراز حساب:
</b>
<span>{{ (sumBs - sumBd) | abs |number_format }}</span>
</p>
</td>
<td class="center">
<p>
<b> وضعیت:
</b>
{% if sumBs > sumBd%}
برداشت
{% elseif sumBs == sumBd %}
تسویه شده
{% else %}
واریز
{% endif %}
</p>
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

View file

@ -255,7 +255,7 @@
</h4>
<ul class="">
<li class="">
{{note}}
{{note | raw}}
</li>
</ul>
{% endif %}

View file

@ -255,7 +255,7 @@
</h4>
<ul class="">
<li class="">
{{note}}
{{note | raw}}
</li>
</ul>
{% endif %}

View file

@ -255,7 +255,7 @@
</h4>
<ul class="">
<li class="">
{{note}}
{{note | raw}}
</li>
</ul>
{% endif %}

View file

@ -236,33 +236,33 @@
{% set originalPrice = item.bs + item.discount %}
{% set unitPrice = originalPrice / item.commodityCount %}
{% endif %}
{{ unitPrice|round|number_format }}
{{ unitPrice|round|number_format }} {{ doc.money.shortName }}
{% else %}
0
0 {{ doc.money.shortName }}
{% endif %}
</td>
{% if printOptions.discountInfo %}
<td class="center item">
{% if item.showPercentDiscount %}
{{ item.discountPercent }}%
({{ (item.bs * item.commodityCount * item.discountPercent / 100)|round|number_format }})
({{ (item.bs * item.commodityCount * item.discountPercent / 100)|round|number_format }} {{ doc.money.shortName }})
{% else %}
{{ item.discount|number_format }}
{{ item.discount|number_format }} {{ doc.money.shortName }}
{% endif %}
</td>
<td class="center item">
{% if item.showPercentDiscount %}
{% set originalPrice = item.bs / (1 - (item.discountPercent / 100)) %}
{{ originalPrice|round|number_format }}
{{ originalPrice|round|number_format }} {{ doc.money.shortName }}
{% else %}
{{ (item.bs + item.discount)|number_format }}
{{ (item.bs + item.discount)|number_format }} {{ doc.money.shortName }}
{% endif %}
</td>
{% endif %}
{% if printOptions.taxInfo %}
<td class="center item">{{ item.tax | number_format}}</td>
<td class="center item">{{ item.tax | number_format}} {{ doc.money.shortName }}</td>
{% endif %}
<td class="center item">{{ item.bs| number_format }}</td>
<td class="center item">{{ item.bs| number_format }} {{ doc.money.shortName }}</td>
</tr>
{% endif %}
{% endfor %}
@ -296,7 +296,7 @@
</h4>
<ul class="">
<li class="">
{{note}}
{{note | raw}}
</li>
</ul>
{% endif %}
@ -306,25 +306,25 @@
<td class="item" style="width:15%;padding:1%">
<h4>
تخفیف:
{{discount | number_format}}
{{discount | number_format}} {{ doc.money.shortName }}
</h4>
<h4>
مالیات:
{{taxAll | number_format}}
{{taxAll | number_format}} {{ doc.money.shortName }}
</h4>
<h4>
حمل و نقل:
{{transfer | number_format}}
{{transfer | number_format}} {{ doc.money.shortName }}
</h4>
{% if doc.amount != (doc.amount + discount) %}
<h4>
جمع بدون تخفیف:
{{ (doc.amount + discount) | number_format}}
{{ (doc.amount + discount) | number_format}} {{ doc.money.shortName }}
</h4>
{% endif %}
<h4>
جمع کل:
{{ doc.amount | number_format }}
{{ doc.amount | number_format }} {{ doc.money.shortName }}
</h4>
</td>
</tr>

View file

@ -1,6 +1,6 @@
{
"name": "hesabix",
"version": "0.48.0",
"version": "0.49.8",
"private": true,
"scripts": {
"dev": "vite",

View file

@ -86,6 +86,11 @@ export default {
returnObject: {
type: Boolean,
default: false
},
tableType: {
type: String,
required: true,
validator: (value) => ['cost', 'income'].includes(value)
}
},
data() {
@ -135,7 +140,7 @@ export default {
async fetchTreeData() {
this.loading = true;
try {
const response = await axios.get('/api/accounting/table/childs/cost');
const response = await axios.get(`/api/accounting/table/childs/${this.tableType}`);
this.treeItems = response.data;
if (this.modelValue) {

View file

@ -100,6 +100,7 @@ const fa_lang = {
ultimate_package: 'بسته‌های نامحدود',
sell_chart: "فروش هفته گذشته",
bankaccounts_transactions: "کارت حساب بانک",
cashdesk_transactions: "کارت حساب صندوق",
print_queue: "صف چاپ",
open_balance: "تراز افتتاحیه",
sell_invoices_long: "فاکتور‌های فروش",

View file

@ -18,7 +18,89 @@
</v-toolbar>
<v-text-field v-model="searchValue" prepend-inner-icon="mdi-magnify" density="compact" hide-details :rounded="false"
placeholder="جست و جو ..."></v-text-field>
placeholder="جست و جو ...">
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="sm" color="primary">
<v-icon>mdi-filter</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.filters')" location="bottom" />
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<v-icon>mdi-filter</v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<!-- فیلتر درختی حسابها -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
فیلتر حساب:
<v-btn
v-if="selectedAccountId"
size="small"
color="primary"
variant="text"
class="ms-2"
@click="resetAccountFilter"
>
<v-icon size="small" class="me-1">mdi-refresh</v-icon>
بازنشانی
</v-btn>
</v-list-item-title>
<hesabdari-tree-view
v-model="selectedAccountId"
:show-sub-tree="true"
:selectable-only="false"
@select="handleAccountSelect"
@account-selected="handleAccountSelected"
/>
</v-list-item>
<v-divider class="my-2"></v-divider>
<!-- فیلتر بازه زمانی -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
</v-list-item-title>
<v-row>
<v-col cols="12">
<v-checkbox
v-model="timeFilters.find(f => f.value === 'custom').checked"
label="بازه زمانی"
@change="handleCustomDateFilterChange"
hide-details
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.from"
label="از تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.to"
label="تا تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
</v-row>
</v-list-item>
<!-- فیلترهای زمانی -->
<v-list-item v-for="(filter, index) in timeFilters.filter(f => f.value !== 'custom')" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
hide-details />
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table :headers="headers" :items="filteredItems" :search="searchValue" :loading="loading"
:header-props="{ class: 'custom-header' }" hover>
@ -151,8 +233,11 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import axios from 'axios'
import moment from 'jalali-moment'
import HesabdariTreeView from '@/components/forms/HesabdariTreeView.vue'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
const searchValue = ref('')
const loading = ref(true)
@ -167,6 +252,22 @@ const snackbar = ref({
})
const plugins = ref({})
// فیلترهای زمانی
const timeFilters = ref([
{ label: 'امروز', value: 'today', checked: false },
{ label: 'این هفته', value: 'week', checked: false },
{ label: 'این ماه', value: 'month', checked: false },
{ label: 'بازه زمانی', value: 'custom', checked: false },
{ label: 'همه', value: 'all', checked: true },
])
const timeFilter = ref('all')
const selectedAccountId = ref(null)
const dateRange = ref({
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
})
const headers = [
{ title: 'وضعیت', key: 'state', sortable: true },
{ title: 'عملیات', key: 'operation' },
@ -181,12 +282,155 @@ const isPluginActive = (plugName) => {
return plugins.value[plugName] !== undefined
}
// متدهای مدیریت فیلتر حساب
const handleAccountSelect = (account) => {
if (account) {
selectedAccountId.value = account.code
loadData()
}
}
const handleAccountSelected = (account) => {
if (account) {
loadData()
}
}
// متد ریست کردن فیلتر حساب
const resetAccountFilter = () => {
selectedAccountId.value = null
loadData()
}
// متدهای مدیریت فیلتر زمانی
const handleDateRangeChange = () => {
if (dateRange.value.from && dateRange.value.to) {
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa')
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa')
if (fromDate.isAfter(toDate)) {
snackbar.value = {
show: true,
message: 'تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد',
color: 'error'
}
dateRange.value = {
from: null,
to: null
}
return
}
loadData()
}
}
const handleCustomDateFilterChange = (checked) => {
if (checked) {
timeFilters.value.forEach(filter => {
if (filter.value !== 'custom') {
filter.checked = false
}
})
timeFilter.value = 'custom'
dateRange.value = {
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
}
loadData()
} else {
timeFilters.value.forEach(filter => {
filter.checked = filter.value === 'all'
})
timeFilter.value = 'all'
dateRange.value = {
from: null,
to: null
}
loadData()
}
}
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value
})
timeFilter.value = value
if (value !== 'custom') {
dateRange.value = {
from: null,
to: null
}
}
loadData()
}
const loadData = async () => {
try {
loading.value = true
const filters = {}
if (searchValue.value.trim()) {
filters.search = { value: searchValue.value.trim() }
}
if (timeFilter.value) {
filters.timeFilter = timeFilter.value
if (timeFilter.value === 'custom' && dateRange.value.from && dateRange.value.to) {
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD')
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD')
filters.date = {
from: fromDate,
to: toDate
}
} else {
const today = moment().locale('fa').format('YYYY/MM/DD')
switch (timeFilter.value) {
case 'today':
filters.date = {
from: today,
to: today
}
break
case 'week':
filters.date = {
from: moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD'),
to: today
}
break
case 'month':
filters.date = {
from: moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD'),
to: today
}
break
}
}
}
if (selectedAccountId.value) {
filters.account = selectedAccountId.value
}
const response = await axios.post('/api/accounting/search', {
type: 'all'
type: 'all',
filters,
pagination: {
page: 1,
limit: 100
},
sort: {
sortBy: 'id',
sortDesc: true
}
})
items.value = response.data.map(item => ({
items.value = response.data.items.map(item => ({
...item,
amount: item.amount.toLocaleString(),
amountRaw: item.amount
@ -195,6 +439,11 @@ const loadData = async () => {
} catch (error) {
console.error('Error loading data:', error)
loading.value = false
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message),
color: 'error'
}
}
}
@ -241,20 +490,17 @@ const confirmDelete = async () => {
deleteLoading.value = true
const response = await axios.delete(`/api/hesabdari/direct/doc/delete/${selectedItem.value.id}`)
if (response.data.success) {
// حذف آیتم از لیست
const index = items.value.findIndex(item => item.id === selectedItem.value.id)
if (index !== -1) {
items.value.splice(index, 1)
}
deleteDialog.value = false
// نمایش پیام موفقیت
snackbar.value = {
show: true,
message: 'سند با موفقیت حذف شد',
color: 'success'
}
} else {
// نمایش پیام خطا
snackbar.value = {
show: true,
message: response.data.message || 'خطا در حذف سند',
@ -262,7 +508,6 @@ const confirmDelete = async () => {
}
}
} catch (error) {
// نمایش پیام خطا
snackbar.value = {
show: true,
message: error.response?.data?.message || 'خطا در ارتباط با سرور',
@ -273,6 +518,13 @@ const confirmDelete = async () => {
}
}
// اضافه کردن watch برای تغییرات تاریخها
watch([() => dateRange.value.from, () => dateRange.value.to], () => {
if (timeFilter.value === 'custom') {
handleDateRangeChange()
}
}, { deep: true })
onMounted(() => {
loadData()
loadPlugins()

View file

@ -1,89 +1,133 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button"
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
تراکنش های صندوق
</h3>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="col-sm-12 col-md-6 mb-1">
<div class="card push">
<div class="card-header border-bottom-0 bg-primary-dark text-light">
<h3 class="block-title"> گردش حساب <small class="text-info-light">{{ selectedObjectItem.name }}</small>
</h3>
<v-toolbar color="toolbar" :title="$t('drawer.cashdesk_transactions')">
<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-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="red">
<v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
<v-icon icon="mdi-file-pdf-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="print(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="print(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="green">
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
<v-icon icon="mdi-file-excel-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="excellOutput(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="excellOutput(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<v-row class="pa-1">
<v-col cols="12" sm="12" md="12">
<v-card :loading="loading">
<v-card-text>
<v-row class="">
<v-col cols="12" sm="12" md="12">
<small class="mb-2">صندوق</small>
<v-cob dir="rtl" :options="objectItems" label="name" v-model="selectedObjectItem"
@option:selected="updateRoute(selectedObjectItem.code)">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
</v-col>
<v-col cols="12" sm="12" md="4">
<div class="fw-bold mb-2">کد حسابداری: <small class="text-primary">{{ selectedObjectItem.code }}</small>
</div>
<div class="card-body">
<small class="mb-2">صندوق</small>
<v-cob dir="rtl" :options="objectItems" label="name" v-model="selectedObjectItem"
@option:selected="updateRoute(selectedObjectItem.code)">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<hr />
<div class="fw-bold mb-2">کد حسابداری: <small class="text-primary">{{ selectedObjectItem.code }}</small>
</div>
<div class="fw-bold mb-2">نام : <small class="text-primary">{{ selectedObjectItem.name }}</small></div>
<div class="fw-bold mb-2">شرح: <small class="text-primary">{{ selectedObjectItem.des }}</small></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<h3>تراکنش ها:</h3>
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
</div>
</div>
<EasyDataTable table-class-name="customize-table" show-index alternating :search-value="searchValue" :headers="headers" :items="items"
theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="loading">
<template #item-operation="{ code }">
<router-link class="text-success" :to="'/acc/accounting/view/' + code">
<i class="fa fa-eye px-1"></i>
</router-link>
<div class="fw-bold mb-2">نام : <small class="text-primary">{{ selectedObjectItem.name }}</small></div>
</v-col>
<v-col cols="12" sm="12" md="4">
<div class="fw-bold mb-2">شرح: <small class="text-primary">{{ selectedObjectItem.des }}</small></div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="12" md="12">
<v-text-field :loading="loading" color="green" class="mb-0 pt-0 rounded-0" hide-details="auto" density="compact"
:placeholder="$t('dialog.search_txt')" v-model="searchValue" type="text" clearable>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify"></v-icon>
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
</v-tooltip>
</template>
</v-text-field>
<EasyDataTable table-class-name="customize-table" show-index alternating v-model:items-selected="itemsSelected"
:search-value="searchValue" :headers="headers" :items="items" theme-color="#1d90ff"
header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading">
<template #item-operation="{ code }">
<router-link class="text-success" :to="'/acc/accounting/view/' + code">
<i class="fa fa-eye px-1"></i>
</router-link>
</template>
</EasyDataTable>
</v-col>
</v-row>
</template>
<script>
import axios from "axios";
import { ref } from "vue";
import Swal from "sweetalert2";
export default {
name: "card",
data: () => {
return {
searchValue: '',
objectItems: [{
name: ''
}],
selectedObjectItem: {},
itemsSelected: [],
items: [],
loading: ref(true),
selectedObjectItem: {
id: '',
code: 0,
name: '',
},
objectItems: [],
loading: true,
headers: [
{ text: "عملیات", value: "operation" },
{ text: "تاریخ", value: "date", 'sortable': true },
{ text: "شرح", value: "des" },
{ text: "تفضیل", value: "ref", 'sortable': true },
{ text: "بدهکار", value: "bd", 'sortable': true },
{ text: ستانکار", value: "bs", 'sortable': true },
{ text: "واریز", value: "bd", 'sortable': true },
{ text: رداشت", value: "bs", 'sortable': true },
]
}
},
@ -96,7 +140,9 @@ export default {
this.loadData();
},
loadData() {
this.loading = true;
axios.post('/api/cashdesk/list').then((response) => {
this.loading = false;
this.objectItems = response.data;
if (this.$route.params.id != '') {
this.loadObject(this.$route.params.id);
@ -126,7 +172,98 @@ export default {
})
this.loading = false;
});
}
},
excellOutput(AllItems = true) {
if (AllItems) {
this.loading = true;
axios({
method: 'post',
url: '/api/cashdesk/card/list/excel',
data: { 'code': this.selectedObjectItem.code },
responseType: 'arraybuffer',
}).then((response) => {
this.loading = false;
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', 'کارت حساب صندوق ' + this.selectedObjectItem.name + '.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
}
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
this.loading = true;
axios({
method: 'post',
url: '/api/cashdesk/card/list/excel',
responseType: 'arraybuffer',
data: {
'code': this.selectedObjectItem.code,
'items': this.itemsSelected
}
}).then((response) => {
this.loading = false;
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', 'کارت حساب صندوق ' + this.selectedObjectItem.name + '.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
}
}
},
print(AllItems = true) {
if (this.selectedObjectItem == null) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
if (AllItems) {
this.loading = true;
axios.post('/api/cashdesk/card/list/print', { 'code': this.selectedObjectItem.code }).then((response) => {
this.printID = response.data.id;
this.loading = false;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
this.loading = true;
axios.post('/api/cashdesk/card/list/print', {
'code': this.selectedObjectItem.code,
'items': this.itemsSelected
}).then((response) => {
this.loading = false;
this.printID = response.data.id;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
}
}
},
}
}
</script>

View file

@ -1,147 +1,329 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button" class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="fa fa-bank px-2"></i>
صندوق ها </h3>
<div class="block-options">
<router-link to="/acc/cashdesk/mod/" class="block-options-item">
<span class="fa fa-plus fw-bolder"></span>
</router-link>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
</div>
</div>
<EasyDataTable table-class-name="customize-table"
show-index
alternating
:search-value="searchValue"
:headers="headers"
:items="items"
theme-color="#1d90ff"
header-text-direction="center"
body-text-direction="center"
rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد"
rowsOfPageSeparatorMessage="از"
:loading = "loading"
>
<template #item-operation="{ code }">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link"
data-bs-toggle="dropdown" id="dropdown-align-center-alt-primary" type="button">
<i class="fa-solid fa-ellipsis"></i>
</button>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end"
style="">
<router-link class="dropdown-item" :to="'/acc/cashdesk/card/view/' + code">
<i class="fa fa-eye text-success pe-2"></i>
مشاهده
</router-link>
<router-link class="dropdown-item" :to="'/acc/cashdesk/mod/' + code">
<i class="fa fa-edit pe-2"></i>
ویرایش
</router-link>
<button type="button" @click="deleteItem(code)" class="dropdown-item text-danger">
<i class="fa fa-trash pe-2"></i>
حذف
</button>
</div>
<v-toolbar color="toolbar" :title="$t('drawer.cashdesks')">
<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-slide-group show-arrows>
<v-slide-group-item>
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/cashdesk/mod/" />
</template>
</v-tooltip>
</v-slide-group-item>
<v-slide-group-item>
<v-tooltip :text="$t('dialog.column_settings')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true" />
</template>
</v-tooltip>
</v-slide-group-item>
</v-slide-group>
</v-toolbar>
<v-text-field
v-model="search"
:loading="loading"
color="green"
class="mb-0 pt-0 rounded-0"
hide-details="auto"
density="compact"
:rounded="false"
:placeholder="$t('dialog.search_txt')"
clearable
>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template>
</v-tooltip>
</template>
</v-text-field>
<v-data-table
:headers="visibleHeaders"
:items="items"
:loading="loading"
:search="search"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
>
<template v-slot:item="{ item }">
<tr>
<td v-if="isColumnVisible('operation')" class="text-center">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<template #item-name="{ name,code }">
<router-link :to="'/acc/cashdesk/card/view/' + code">
{{name}}
</router-link>
</template>
<template #item-balance="{ balance }">
<label class="text-success" v-if="balance >= 0">{{ $filters.formatNumber(balance)}}</label>
<label class="text-danger" v-else>{{ $filters.formatNumber( -1 * balance ) }} منفی</label>
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
<v-list>
<v-list-item :to="'/acc/cashdesk/card/view/' + item.code">
<template v-slot:prepend>
<v-icon color="success" icon="mdi-eye" />
</template>
<v-list-item-title>{{ $t('dialog.view') }}</v-list-item-title>
</v-list-item>
<v-list-item :to="'/acc/cashdesk/mod/' + item.code">
<template v-slot:prepend>
<v-icon icon="mdi-pencil" />
</template>
<v-list-item-title>{{ $t('dialog.edit') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmDelete(item.code)">
<template v-slot:prepend>
<v-icon color="error" icon="mdi-delete" />
</template>
<v-list-item-title>{{ $t('dialog.delete') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</td>
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
<td v-if="isColumnVisible('name')" class="text-center">
<router-link :to="'/acc/cashdesk/card/view/' + item.code">
{{ item.name }}
</router-link>
</td>
<td v-if="isColumnVisible('balance')" class="text-center">
<span :class="Number(item.balance) >= 0 ? 'text-success' : 'text-error'">
{{ formatNumber(Math.abs(Number(item.balance))) }}
<span v-if="Number(item.balance) < 0">منفی</span>
</span>
</td>
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
</tr>
</template>
</v-data-table>
<v-dialog v-model="showColumnDialog" max-width="500">
<v-card>
<v-toolbar color="toolbar" :title="$t('dialog.manage_columns')">
<v-spacer></v-spacer>
<v-btn icon @click="showColumnDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col v-for="header in allHeaders" :key="header.key" cols="12" sm="6">
<v-checkbox
v-model="header.visible"
:label="header.title"
@change="updateColumnVisibility"
hide-details
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title class="text-h6">
تأیید حذف
</v-card-title>
<v-card-text>
آیا برای حذف صندوق مطمئن هستید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">خیر</v-btn>
<v-btn color="error" variant="text" @click="deleteItem">بله</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="messageDialog.show" max-width="400">
<v-card>
<v-card-title :class="messageDialog.color + ' text-h6'">
{{ messageDialog.title }}
</v-card-title>
<v-card-text class="pt-4">
{{ messageDialog.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="messageDialog.show = false">قبول</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
import {ref} from "vue";
<script setup>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
export default {
name: "list",
data: ()=>{return {
searchValue: '',
loading: ref(true),
items:[],
headers: [
{ text: "عملیات", value: "operation", width: "130"},
{ text: "کد", value: "code", width: "100px" },
{ text: "نام صندوق", value: "name", width: "120px"},
{ text: "موجودی()", value: "balance", width: "140px"},
{ text: "توضیحات", value: "des", width: "150px"},
]
}},
methods: {
loadData(){
axios.post('/api/cashdesk/list')
.then((response)=>{
this.items = response.data;
this.loading = false;
})
},
deleteItem(code) {
Swal.fire({
text: 'آیا برای حذف صندوق مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: `خیر`,
}).then((result) => {
/* Read more about isConfirmed, isDenied below */
if (result.isConfirmed) {
axios.post('/api/cashdesk/delete/' + code).then((response) => {
if (response.data.result == 1) {
let index = 0;
for (let z = 0; z < this.items.length; z++) {
index++;
if (this.items[z]['code'] == code) {
this.items.splice(index - 1, 1);
}
}
Swal.fire({
text: 'صندوق با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
else if (response.data.result == 2) {
Swal.fire({
text: 'صندوق به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
})
}
})
}
},
beforeMount() {
this.loadData();
// Refs
const loading = ref(false);
const items = ref([]);
const search = ref('');
const showColumnDialog = ref(false);
// دیالوگها
const deleteDialog = ref({
show: false,
code: null
});
const messageDialog = ref({
show: false,
title: '',
message: '',
color: 'primary'
});
// تابع فرمتکننده اعداد
const formatNumber = (value) => {
if (!value) return '0';
return Number(value).toLocaleString('fa-IR');
};
// تعریف همه ستونها با align مرکز
const allHeaders = ref([
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
{ title: "کد", key: "code", align: 'center', sortable: true, width: 100, visible: true },
{ title: "نام صندوق", key: "name", align: 'center', sortable: true, width: 140, visible: true },
{ title: "موجودی", key: "balance", align: 'center', sortable: true, width: 140, visible: true },
{ title: "توضیحات", key: "des", align: 'center', sortable: true, width: 150, visible: true },
]);
// ستونهای قابل نمایش
const visibleHeaders = computed(() => {
return allHeaders.value.filter(header => header.visible);
});
// بررسی نمایش ستون
const isColumnVisible = (key) => {
return allHeaders.value.find(header => header.key === key)?.visible;
};
// کلید ذخیرهسازی در localStorage
const LOCAL_STORAGE_KEY = 'hesabix_cashdesk_table_columns';
// لود تنظیمات ستونها
const loadColumnSettings = () => {
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSettings) {
const visibleColumns = JSON.parse(savedSettings);
allHeaders.value.forEach(header => {
header.visible = visibleColumns.includes(header.key);
});
}
}
};
// ذخیره تنظیمات ستونها
const updateColumnVisibility = () => {
const visibleColumns = allHeaders.value
.filter(header => header.visible)
.map(header => header.key);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(visibleColumns));
};
// نمایش پیام
const showMessage = (message, title = 'پیام', color = 'primary') => {
messageDialog.value = {
show: true,
title,
message,
color
};
};
// تأیید حذف
const confirmDelete = (code) => {
deleteDialog.value = {
show: true,
code
};
};
// بارگذاری دادهها
const loadData = async () => {
loading.value = true;
try {
const response = await axios.post('/api/cashdesk/list');
items.value = response.data;
} catch (error) {
console.error('Error loading data:', error);
showMessage('خطا در بارگذاری داده‌ها: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// حذف آیتم
const deleteItem = async () => {
const code = deleteDialog.value.code;
deleteDialog.value.show = false;
if (!code) return;
try {
loading.value = true;
const response = await axios.post(`/api/cashdesk/delete/${code}`);
if (response.data.result === 1) {
items.value = items.value.filter(item => item.code !== code);
showMessage('صندوق با موفقیت حذف شد.', 'موفقیت', 'success');
} else if (response.data.result === 2) {
showMessage('صندوق به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.', 'خطا', 'error');
}
} catch (error) {
console.error('Error deleting item:', error);
showMessage('خطا در حذف آیتم: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// مانت کامپوننت
onMounted(() => {
loadColumnSettings();
loadData();
});
</script>
<style scoped>
<style>
.v-data-table {
width: 100%;
overflow-x: auto;
}
/* استایل برای وسط‌چین کردن همه سلول‌های جدول */
:deep(.v-data-table-header th) {
text-align: center !important;
}
:deep(.v-data-table__wrapper table td) {
text-align: center !important;
}
/* استایل برای رنگ‌های متن */
.text-success {
color: #4caf50 !important;
}
.text-error {
color: #ff5252 !important;
}
/* استایل برای لینک‌ها در جدول */
:deep(.v-data-table__wrapper table td a) {
text-decoration: none;
color: #1976d2;
}
:deep(.v-data-table__wrapper table td a:hover) {
text-decoration: underline;
}
</style>

View file

@ -103,7 +103,7 @@
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<Htabletreeselect v-model="item.id" :items="listscosts" label="مرکز هزینه" />
<Htabletreeselect v-model="item.id" label="مرکز هزینه" tableType="cost" />
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
@ -360,7 +360,6 @@ export default {
sum: 0,
balance: 0,
listPersons: [],
listscosts: [],
listBanks: [],
listCashdesks: [],
listSalarys: [],
@ -398,12 +397,7 @@ export default {
beforeRouteUpdate(to, from) {
this.loadData(to.params.id);
},
computed: {
formattedCostItems() {
// تبدیل ساختار درختی به آرایه ساده
return this.flattenCostItems(this.listscosts);
}
},
methods: {
calc() {
this.sum = 0;
@ -553,10 +547,6 @@ export default {
this.data.date = response.data.now;
})
}
//get list of items
axios.post('/api/accounting/table/childs/cost').then((response) => {
this.listscosts = response.data;
});
//get list of banks
axios.post('/api/bank/list').then((response) => {

File diff suppressed because it is too large Load diff

View file

@ -94,8 +94,7 @@
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-textarea v-model="settings.sell.noteString" label="یادداشت پایین فاکتور" rows="4"
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
<CustomEditor title="یادداشت پایین فاکتور" v-model="settings.sell.noteString" />
</v-col>
<v-col cols="12" md="6">
<v-select v-model="settings.sell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -130,8 +129,7 @@
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-textarea v-model="settings.buy.noteString" label="یادداشت پایین فاکتور" rows="4"
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
<CustomEditor title="یادداشت پایین فاکتور" v-model="settings.buy.noteString" />
</v-col>
<v-col cols="12" md="6">
<v-select v-model="settings.buy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -166,8 +164,7 @@
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-textarea v-model="settings.rfbuy.noteString" label="یادداشت پایین فاکتور" rows="4"
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
<CustomEditor title="یادداشت پایین فاکتور" v-model="settings.rfbuy.noteString" />
</v-col>
<v-col cols="12" md="6">
<v-select v-model="settings.rfbuy.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>
@ -202,8 +199,7 @@
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-textarea v-model="settings.rfsell.noteString" label="یادداشت پایین فاکتور" rows="4"
placeholder="این نوشته در پایین فاکتور‌ها چاپ خواهد شد"></v-textarea>
<CustomEditor title="یادداشت پایین فاکتور" v-model="settings.rfsell.noteString" />
</v-col>
<v-col cols="12" md="6">
<v-select v-model="settings.rfsell.paper" :items="paperOptions" label="سایز کاغذ و حالت چاپ"></v-select>