progress in salary and add export pdf/excell to that

This commit is contained in:
Hesabix 2025-07-25 09:54:43 +00:00
parent 8bc857c2f8
commit cc1515345b
4 changed files with 818 additions and 238 deletions

View file

@ -14,6 +14,9 @@ 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 Symfony\Component\HttpFoundation\BinaryFileResponse;
class SalaryController extends AbstractController
{
@ -66,7 +69,21 @@ class SalaryController extends AbstractController
'bid' => $acc['bid'],
'code' => $code
]);
return $this->json(Explore::ExploreSalary($data));
$result = Explore::ExploreSalary($data);
// محاسبه بدهکار و بستانکار و تراز
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $bd - $bs;
return $this->json($result);
}
#[Route('/api/salary/mod/{code}', name: 'app_salary_mod')]
@ -267,4 +284,135 @@ class SalaryController extends AbstractController
'total' => count($transactions)
]);
}
/**
* خروجی اکسل کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/excel', name: 'app_salary_card_list_excel')]
public function app_salary_card_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|Response
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
} else {
$transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
}
}
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
[
'شماره تراکنش',
'تاریخ',
'توضیحات',
'تفضیل',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getRef() ? $transaction->getRef()->getName() : '',
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear() ? $transaction->getYear()->getlabel() : '',
];
}
$activeWorksheet->fromArray($arrayEntity, null, 'A1');
$activeWorksheet->setRightToLeft(true);
$writer = new Xlsx($spreadsheet);
$filePath = __DIR__ . '/../../var/' . uniqid('salary_card_', true) . '.xlsx';
$writer->save($filePath);
return new BinaryFileResponse($filePath);
}
/**
* خروجی PDF کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/print', name: 'app_salary_card_list_print')]
public function app_salary_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
} else {
$transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
}
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/salary_card.html.twig', [
'page_title' => 'کارت حساب تنخواه گردان ' . $salary->getName(),
'bid' => $acc['bid'],
'items' => $transactions,
'salary' => $salary
])
);
return $this->json(['id' => $pid]);
}
}

View file

@ -0,0 +1,105 @@
{% 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>
{{ salary.name }}
</p>
</td>
<td class="center">
<p>
<b>کد حسابداری:</b>
{{ salary.code }}
</p>
</td>
<td class="center" colspan="3">
<p>
<b>شرح:</b>
{{ salary.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>
</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.ref ? 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.doc.des }}</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

@ -1,65 +1,147 @@
<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>
</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>
<v-toolbar color="toolbar" dense flat>
<v-btn icon @click="$router.back()" class="d-none d-md-flex">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
<v-toolbar-title class="text-primary-dark">
کارت حساب تنخواه گردان
</v-toolbar-title>
<v-spacer />
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="red">
<v-tooltip activator="parent" text="خروجی PDF" location="bottom" />
<v-icon icon="mdi-file-pdf-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">خروجی PDF</v-list-subheader>
<v-list-item class="text-dark" title="انتخاب شده‌ها" @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="همه" @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="خروجی اکسل" location="bottom" />
<v-icon icon="mdi-file-excel-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">خروجی اکسل</v-list-subheader>
<v-list-item class="text-dark" title="انتخاب شده‌ها" @click="excelOutput(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="همه" @click="excelOutput(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>
</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="جست و جو ...">
<v-container fluid class="pa-4">
<v-row dense>
<v-col cols="12" md="12">
<v-autocomplete v-model="selectedSalary" :items="listSalaries" item-title="name" item-value="code"
return-object label="انتخاب تنخواه گردان" dense hide-details prepend-inner-icon="mdi-cash"
:loading="loading" @update:model-value="updateRoute"
class="rounded-lg elevation-2">
<template v-slot:no-data>
نتیجهای یافت نشد!
</template>
</v-autocomplete>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
اطلاعات تنخواه گردان
<small class="text-info-light" v-if="selectedSalary">{{ selectedSalary.name }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">کد حسابداری: <span class="text-primary">{{ selectedSalary.code || '-' }}</span></div>
<div class="text-subtitle-2">نام: <span class="text-primary">{{ selectedSalary.name || '-' }}</span></div>
<div class="text-subtitle-2">شرح: <span class="text-primary">{{ selectedSalary.des || '-' }}</span></div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
وضعیت حساب
<small class="text-info-light" v-if="selectedSalary">{{ selectedSalary.name }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">
وضعیت حساب:
<span :class="{
'text-success': selectedSalary.balance > 0,
'text-danger': selectedSalary.balance < 0,
'text-dark': selectedSalary.balance == 0
}">
{{ selectedSalary.balance > 0 ? 'بستانکار' : selectedSalary.balance < 0 ? 'بدهکار' : 'تسویه' }}
</span>
</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>
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
<div class="text-subtitle-2">بستانکار: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.bs) || '-' }}</span></div>
<div class="text-subtitle-2">بدهکار: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.bd) || '-' }}</span></div>
<div class="text-subtitle-2">تراز حساب: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.balance) || '-' }}</span></div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12">
<v-data-table v-model="itemsSelected" :headers="headers" :items="items" :search="searchValue" :loading="loading"
show-select dense :items-per-page="25" class="elevation-2 rounded-lg" :header-props="{ class: 'custom-header' }">
<template v-slot:top>
<v-toolbar flat dense color="grey-lighten-4" class="rounded-t-lg">
<v-toolbar-title class="text-subtitle-1">تراکنشها</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field v-model="searchValue" dense hide-details prepend-inner-icon="mdi-magnify" />
</v-toolbar>
</template>
<template v-slot:item.operation="{ item }">
<v-btn variant="plain" icon size="small" :to="'/acc/accounting/view/' + item.code" color="success">
<v-icon small>mdi-eye</v-icon>
</v-btn>
</template>
<template v-slot:item.code="{ item }">
{{ $filters.formatNumber(item.code) }}
</template>
<template v-slot:item.bd="{ item }">
{{ $filters.formatNumber(item.bd) }}
</template>
<template v-slot:item.bs="{ item }">
{{ $filters.formatNumber(item.bs) }}
</template>
<template v-slot:no-data>
اطلاعاتی برای نمایش وجود ندارد
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar v-model="snackbar" :timeout="3000" color="info">{{ snackbarText }}</v-snackbar>
<v-overlay :value="loading" contained class="align-center justify-center">
<v-progress-circular indeterminate size="64" />
</v-overlay>
</template>
<script>
@ -68,67 +150,151 @@ import { ref } from "vue";
export default {
name: "card",
data: () => {
data() {
return {
searchValue: '',
objectItems: [{
name: ''
}],
selectedObjectItem: {},
listSalaries: [],
itemsSelected: [],
selectedSalary: { balance: 0, bs: 0, bd: 0 },
items: [],
loading: ref(true),
loading: ref(false),
snackbar: false,
snackbarText: '',
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 },
]
}
{ title: 'عملیات', key: "operation", align: "center", sortable: false },
{ title: 'شماره سند', key: "code", align: "center", sortable: true },
{ title: 'تاریخ', key: "date", align: "center", sortable: true },
{ title: 'شرح', key: "des", align: "center" },
{ title: 'تفضیل', key: "ref", align: "center", sortable: true },
{ title: 'بدهکار', key: "bd", align: "center", sortable: true },
{ title: 'بستانکار', key: "bs", align: "center", sortable: true },
],
};
},
mounted() {
this.loadData();
},
methods: {
updateRoute(id) {
this.$router.push(id);
this.loadData();
showSnackbar(text) {
this.snackbarText = text;
this.snackbar = true;
},
loadData() {
axios.post('/api/salary/list').then((response) => {
this.objectItems = response.data;
if (this.$route.params.id != '') {
this.loadObject(this.$route.params.id);
this.objectItems.forEach((item) => {
if (item.code == this.$route.params.id) {
this.selectedObjectItem = item;
}
});
} else {
this.selectedObjectItem = response.data[0];
this.loadObject(this.selectedObjectItem.code);
}
});
updateRoute() {
if (this.selectedSalary && this.selectedSalary.code) {
this.$router.push(this.selectedSalary.code);
this.loadSalary(this.selectedSalary.code);
}
},
loadObject(id) {
async loadData() {
this.loading = true;
axios.post('/api/accounting/rows/search',
{
type: 'salary',
id: id
try {
const response = await axios.post('/api/salary/list');
this.listSalaries = response.data;
const id = this.$route.params.id;
if (id) {
await this.loadSalary(id);
} else if (response.data.length > 0) {
this.selectedSalary = response.data[0];
await this.loadSalary(this.selectedSalary.code);
}
).then((response) => {
this.items = response.data;
this.items.forEach((item) => {
item.bs = this.$filters.formatNumber(item.bs)
item.bd = this.$filters.formatNumber(item.bd)
})
} catch (error) {
this.showSnackbar('خطا در بارگذاری اطلاعات');
} finally {
this.loading = false;
});
}
}
}
}
},
async loadSalary(id) {
this.loading = true;
try {
const salaryResponse = await axios.post('/api/salary/info/' + id);
this.selectedSalary = salaryResponse.data;
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'salary', id });
this.items = rowsResponse.data;
} catch (error) {
this.selectedSalary = { balance: 0, bs: 0, bd: 0 };
this.items = [];
this.showSnackbar('خطا در دریافت اطلاعات تنخواه گردان');
} finally {
this.loading = false;
}
},
async excelOutput(allItems = true) {
if (!allItems && this.itemsSelected.length === 0) {
this.showSnackbar('هیچ تراکنشی انتخاب نشده است');
return;
}
try {
const response = await axios({
method: 'post',
url: '/api/salary/card/list/excel',
data: allItems ? { code: this.selectedSalary.code } : { code: this.selectedSalary.code, items: this.itemsSelected },
responseType: 'arraybuffer',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'salary-card-view.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
this.showSnackbar('خطا در دریافت خروجی اکسل');
}
},
async print(allItems = true) {
if (!this.selectedSalary) {
this.showSnackbar('هیچ تنخواه گردانی انتخاب نشده است');
return;
}
if (!allItems && this.itemsSelected.length === 0) {
this.showSnackbar('هیچ تراکنشی انتخاب نشده است');
return;
}
try {
const response = await axios.post('/api/salary/card/list/print', allItems ? { code: this.selectedSalary.code } : { code: this.selectedSalary.code, items: this.itemsSelected });
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
} catch (error) {
this.showSnackbar('خطا در دریافت خروجی PDF');
}
},
},
};
</script>
<style scoped></style>
<style scoped>
.custom-header {
background-color: #f5f5f5 !important;
font-weight: bold !important;
}
.v-data-table {
border-radius: 8px;
overflow: hidden;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.v-autocomplete {
background-color: white;
border-radius: 8px;
}
.v-toolbar {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.v-list-item {
transition: background-color 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
</style>

View file

@ -1,138 +1,299 @@
<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/salary/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/salary/card/view/' + code">
<i class="fa fa-eye text-success pe-2"></i>
مشاهده
</router-link>
<router-link class="dropdown-item" :to="'/acc/salary/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.salary')">
<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/salary/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/salary/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/salary/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/salary/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/salary/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: "70px" },
{ text: "نام تنخواه‌گردان", value: "name", width: "120px" },
{ text: "موجودی()", value: "balance", width: "140px" },
{ text: "توضیحات", value: "des", width: "150px" },
]
}
},
methods: {
loadData() {
axios.post('/api/salary/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/salary/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();
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');
};
const allHeaders = ref([
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
{ title: "کد", key: "code", align: 'center', sortable: true, width: 70, visible: true },
{ title: "نام تنخواه‌گردان", key: "name", align: 'center', sortable: true, width: 120, 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;
};
const LOCAL_STORAGE_KEY = 'hesabix_salary_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/salary/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/salary/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>
<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>