progress in sustome controls

This commit is contained in:
Hesabix 2025-04-04 11:26:49 +00:00
parent 2d0305f918
commit 2fb2449207
5 changed files with 826 additions and 22 deletions

View file

@ -12,6 +12,13 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Service\Provider;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Salary;
use App\Entity\Person;
use App\Service\Log;
class CostController extends AbstractController
{
@ -467,8 +474,6 @@ class CostController extends AbstractController
$paymentCenter = $row->getCashdesk()->getName();
} elseif ($row->getSalary()) {
$paymentCenter = $row->getSalary()->getName();
} elseif ($row->getCommodity()) {
$paymentCenter = $row->getCommodity()->getName();
} elseif ($row->getPerson()) {
$paymentCenter = $row->getPerson()->getNikename();
}
@ -492,4 +497,138 @@ class CostController extends AbstractController
return new BinaryFileResponse($filePath);
}
#[Route('/api/cost/doc/insert', name: 'app_cost_doc_insert', methods: ['POST'])]
public function insertCostDoc(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Provider $provider,
Log $log,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('cost');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
// بررسی پارامترهای ضروری
if (!isset($params['rows']) || count($params['rows']) < 2) {
return $this->json(['result' => 0, 'message' => 'حداقل دو ردیف برای سند هزینه الزامی است'], 400);
}
if (!isset($params['date']) || !isset($params['des'])) {
return $this->json(['result' => 0, 'message' => 'تاریخ و شرح سند الزامی است'], 400);
}
// تنظیم نوع سند به cost
$params['type'] = 'cost';
// بررسی وجود سند برای ویرایش
if (isset($params['update']) && $params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['update'],
'money' => $acc['money']
]);
if (!$doc) {
return $this->json(['result' => 0, 'message' => 'سند مورد نظر یافت نشد'], 404);
}
}
// ایجاد سند جدید
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDes($params['des']);
$doc->setDateSubmit(time());
$doc->setType('cost');
$doc->setDate($params['date']);
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$entityManager->persist($doc);
$entityManager->flush();
// پردازش ردیف‌های سند
$amount = 0;
foreach ($params['rows'] as $row) {
$row['bs'] = str_replace(',', '', $row['bs']);
$row['bd'] = str_replace(',', '', $row['bd']);
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
// تنظیم مرکز هزینه
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => $row['table']
]);
$hesabdariRow->setRef($ref);
// تنظیم مرکز پرداخت (بانک، صندوق، تنخواه، شخص)
if ($row['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$bank) {
return $this->json(['result' => 0, 'message' => 'حساب بانکی مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setBank($bank);
} elseif ($row['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['id']);
if (!$cashdesk) {
return $this->json(['result' => 0, 'message' => 'صندوق مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setCashdesk($cashdesk);
} elseif ($row['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->find($row['id']);
if (!$salary) {
return $this->json(['result' => 0, 'message' => 'تنخواه مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setSalary($salary);
} elseif ($row['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$person) {
return $this->json(['result' => 0, 'message' => 'شخص مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setPerson($person);
}
if (isset($row['des'])) {
$hesabdariRow->setDes($row['des']);
}
$entityManager->persist($hesabdariRow);
$amount += $row['bs'];
}
$doc->setAmount($amount);
$entityManager->persist($doc);
$entityManager->flush();
$log->insert(
'حسابداری',
'سند هزینه شماره ' . $doc->getCode() . ' ثبت شد.',
$this->getUser(),
$acc['bid'],
$doc
);
return $this->json([
'result' => 1,
'doc' => $provider->Entity2Array($doc, 0)
]);
}
}

View file

@ -426,11 +426,12 @@ class HesabdariController extends AbstractController
$entityManager->flush();
$hesabdariRow->setCheque($cheque);
} elseif ($row['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->find($row['id']);
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$bank)
throw $this->createNotFoundException('bank not found');
elseif ($bank->getBid()->getId() != $acc['bid']->getId())
throw $this->createAccessDeniedException('bank is not in this business');
$hesabdariRow->setBank($bank);
} elseif ($row['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->find($row['id']);

View file

@ -0,0 +1,667 @@
<template>
<div>
<v-menu v-model="menu" :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-text-field
v-bind="props"
v-model="displayValue"
variant="outlined"
hide-details
density="compact"
:label="label"
class=""
prepend-inner-icon="mdi-account"
clearable
@click:clear="clearSelection"
:loading="loading"
@keydown.enter="handleEnter"
>
<template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template>
</v-text-field>
</template>
<v-card min-width="300" max-width="400">
<v-card-text class="pa-2">
<template v-if="!loading">
<v-list density="compact" class="list-container">
<template v-if="filteredItems.length > 0">
<v-list-item
v-for="item in filteredItems"
:key="item.id"
@click="selectItem(item)"
class="mb-1"
>
<v-list-item-title class="text-right">{{ item.nikename }}</v-list-item-title>
<v-list-item-subtitle class="text-right">{{ item.code }}</v-list-item-subtitle>
</v-list-item>
</template>
<template v-else>
<v-list-item>
<v-list-item-title class="text-center text-grey">
نتیجهای یافت نشد
</v-list-item-title>
</v-list-item>
</template>
</v-list>
<v-btn
v-if="filteredItems.length === 0"
block
color="primary"
class="mt-2"
@click="showAddDialog = true"
>
افزودن کاربر جدید
</v-btn>
</template>
<v-progress-circular
v-else
indeterminate
color="primary"
class="d-flex mx-auto my-4"
></v-progress-circular>
</v-card-text>
</v-card>
</v-menu>
<v-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="800">
<v-card>
<v-toolbar color="primary" density="compact" class="sticky-toolbar">
<v-toolbar-title>افزودن کاربر جدید</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip text="بستن">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-close"
v-bind="props"
@click="showAddDialog = false"
></v-btn>
</template>
</v-tooltip>
<v-tooltip text="ذخیره">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-content-save"
v-bind="props"
@click="savePerson"
:loading="saving"
></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-tabs
v-model="tabs"
color="primary"
show-arrows
class="sticky-tabs"
>
<v-tab value="basic" class="flex-grow-1">اطلاعات پایه</v-tab>
<v-tab value="contact" class="flex-grow-1">اطلاعات تماس</v-tab>
<v-tab value="address" class="flex-grow-1">آدرس</v-tab>
<v-tab value="bank" class="flex-grow-1">حسابهای بانکی</v-tab>
</v-tabs>
<v-card-text class="content-container">
<v-window v-model="tabs">
<v-window-item value="basic">
<v-form @submit.prevent="savePerson">
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.nikename"
label="نام مستعار *"
required
:error-messages="nikenameErrors"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.name"
label="نام و نام خانوادگی"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.company"
label="شرکت"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.des"
label="توضیحات"
></v-text-field>
</v-col>
<v-col cols="12">
<v-switch
v-model="newPerson.speedAccess"
label="دسترسی سریع"
color="primary"
></v-switch>
</v-col>
<v-col cols="12">
<v-card variant="outlined" class="pa-2">
<v-card-title class="text-subtitle-1">نوع مشتری</v-card-title>
<v-card-text class="pa-0">
<v-row dense>
<v-col v-for="(type, index) in personTypes" :key="type.code" cols="12" sm="6" md="4">
<v-checkbox
v-model="newPerson.types[index].checked"
:label="type.label"
color="primary"
density="compact"
hide-details
></v-checkbox>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</v-window-item>
<v-window-item value="contact">
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.mobile"
label="موبایل"
:error-messages="mobileErrors"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.mobile2"
label="موبایل دوم"
:error-messages="mobile2Errors"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.tel"
label="تلفن"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.fax"
label="فکس"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.email"
label="ایمیل"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.website"
label="وب سایت"
></v-text-field>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="address">
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.keshvar"
label="کشور"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.ostan"
label="استان"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.shahr"
label="شهر"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newPerson.postalcode"
label="کد پستی"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newPerson.address"
label="آدرس"
rows="3"
></v-textarea>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="bank">
<v-row class="mt-4">
<v-col cols="12">
<v-btn
color="primary"
@click="addNewBankAccount"
prepend-icon="mdi-plus"
>
افزودن حساب بانکی
</v-btn>
</v-col>
<v-col cols="12" v-for="(account, index) in newPerson.accounts" :key="index">
<v-card variant="outlined" class="mb-4">
<v-card-title class="d-flex justify-space-between align-center">
<span>حساب بانکی {{ index + 1 }}</span>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
@click="removeBankAccount(index)"
></v-btn>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="account.bank"
label="نام بانک *"
required
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="account.accountNum"
label="شماره حساب"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="account.cardNum"
label="شماره کارت"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="account.shabaNum"
label="شماره شبا"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Hpersonsearch',
props: {
modelValue: {
type: [Object, Number],
default: null
},
label: {
type: String,
default: 'شخص'
},
returnObject: {
type: Boolean,
default: false
}
},
data() {
return {
selectedItem: null,
items: [],
loading: false,
menu: false,
searchQuery: '',
totalItems: 0,
currentPage: 1,
itemsPerPage: 10,
searchTimeout: null,
showAddDialog: false,
tabs: 'basic',
saving: false,
personTypes: [],
snackbar: {
show: false,
text: '',
color: 'success'
},
newPerson: {
nikename: '',
name: '',
des: '',
tel: '',
mobile: '',
mobile2: '',
address: '',
company: '',
shenasemeli: '',
codeeghtesadi: '',
sabt: '',
keshvar: '',
ostan: '',
shahr: '',
postalcode: '',
email: '',
website: '',
fax: '',
code: 0,
types: [],
accounts: [],
speedAccess: false
}
};
},
computed: {
filteredItems() {
return Array.isArray(this.items) ? this.items : [];
},
displayValue: {
get() {
if (this.menu) {
return this.searchQuery;
}
return this.selectedItem ? this.selectedItem.nikename : this.searchQuery;
},
set(value) {
this.searchQuery = value;
if (!value) {
this.clearSelection();
}
}
},
nikenameErrors() {
if (!this.newPerson.nikename) return ['نام مستعار الزامی است'];
return [];
},
mobileErrors() {
if (this.newPerson.mobile && !/^09\d{9}$/.test(this.newPerson.mobile)) {
return ['شماره موبایل باید با 09 شروع شود و 11 رقم باشد'];
}
return [];
},
mobile2Errors() {
if (this.newPerson.mobile2 && !/^09\d{9}$/.test(this.newPerson.mobile2)) {
return ['شماره موبایل دوم باید با 09 شروع شود و 11 رقم باشد'];
}
return [];
}
},
watch: {
modelValue: {
handler(newVal) {
if (this.returnObject) {
this.selectedItem = newVal;
} else {
this.selectedItem = this.items.find(item => item.id === newVal);
}
},
immediate: true
},
searchQuery: {
handler(newVal) {
this.currentPage = 1;
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.fetchData();
}, 500);
}
},
showAddDialog: {
handler(newVal) {
if (newVal) {
this.newPerson.nikename = this.searchQuery;
}
}
},
'newPerson.mobile': {
handler(newVal) {
if (newVal && !newVal.startsWith('09')) {
this.newPerson.mobile = '09' + newVal.replace(/^09/, '');
}
}
},
'newPerson.mobile2': {
handler(newVal) {
if (newVal && !newVal.startsWith('09')) {
this.newPerson.mobile2 = '09' + newVal.replace(/^09/, '');
}
}
}
},
methods: {
showMessage(text, color = 'success') {
this.snackbar.text = text;
this.snackbar.color = color;
this.snackbar.show = true;
},
async fetchData() {
this.loading = true;
try {
const response = await axios.post('/api/person/list', {
page: this.currentPage,
itemsPerPage: this.itemsPerPage,
search: this.searchQuery,
types: null,
transactionFilters: null,
sortBy: null
});
console.log('پاسخ API:', response.data);
if (response.data && Array.isArray(response.data)) {
this.items = response.data;
this.totalItems = response.data.length;
} else if (response.data && response.data.items) {
this.items = response.data.items;
this.totalItems = response.data.total || response.data.items.length;
} else {
this.items = [];
this.totalItems = 0;
}
console.log('آیتم‌های ذخیره شده:', this.items);
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue;
} else {
this.selectedItem = this.items.find(item => item.id === this.modelValue);
}
}
} catch (error) {
console.error('خطا در دریافت داده‌ها:', error);
this.showMessage('خطا در بارگذاری داده‌ها', 'error');
this.items = [];
this.totalItems = 0;
} finally {
this.loading = false;
}
},
async loadPersonTypes() {
try {
const response = await axios.post('/api/person/types/get');
this.personTypes = response.data;
this.newPerson.types = response.data.map(type => ({
...type,
checked: false
}));
} catch (error) {
console.error('خطا در دریافت انواع شخص:', error);
this.showMessage('خطا در بارگذاری انواع شخص', 'error');
}
},
addNewBankAccount() {
this.newPerson.accounts.push({
bank: '',
accountNum: '',
cardNum: '',
shabaNum: ''
});
},
removeBankAccount(index) {
this.newPerson.accounts.splice(index, 1);
},
async savePerson() {
if (!this.newPerson.nikename) {
this.showMessage('نام مستعار الزامی است', 'error');
return;
}
if (this.newPerson.mobile && !/^09\d{9}$/.test(this.newPerson.mobile)) {
this.showMessage('شماره موبایل باید با 09 شروع شود و 11 رقم باشد', 'error');
return;
}
if (this.newPerson.mobile2 && !/^09\d{9}$/.test(this.newPerson.mobile2)) {
this.showMessage('شماره موبایل دوم باید با 09 شروع شود و 11 رقم باشد', 'error');
return;
}
for (const account of this.newPerson.accounts) {
if (!account.bank) {
this.showMessage('نام بانک برای حساب‌های بانکی الزامی است', 'error');
return;
}
}
this.saving = true;
try {
const response = await axios.post('/api/person/mod/' + this.newPerson.code, this.newPerson);
if (response.data.Success) {
if (response.data.result === 1) {
this.showMessage('شخص با موفقیت ثبت شد');
this.showAddDialog = false;
this.fetchData();
} else if (response.data.result === 2) {
this.showMessage('این شخص قبلاً ثبت شده است', 'error');
}
} else {
this.showMessage('خطا در ثبت شخص', 'error');
}
} catch (error) {
console.error('خطا در ثبت شخص:', error);
this.showMessage('خطا در ثبت شخص', 'error');
} finally {
this.saving = false;
}
},
selectItem(item) {
this.selectedItem = item;
this.searchQuery = item.nikename;
this.$emit('update:modelValue', this.returnObject ? item : item.id);
this.menu = false;
},
clearSelection() {
this.selectedItem = null;
this.searchQuery = '';
this.$emit('update:modelValue', null);
},
handleEnter() {
if (!this.loading && this.filteredItems.length === 0) {
this.showAddDialog = true;
}
}
},
created() {
this.fetchData();
this.loadPersonTypes();
}
};
</script>
<style scoped>
.list-container {
max-height: 300px;
overflow-y: auto;
}
.content-container {
max-height: 500px;
overflow-y: auto;
}
.sticky-toolbar {
position: sticky;
top: 0;
z-index: 1;
}
.sticky-tabs {
position: sticky;
top: 48px;
z-index: 1;
overflow-x: auto;
white-space: nowrap;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
:deep(.v-window-item[value="contact"] .v-text-field input) {
text-align: left !important;
direction: ltr !important;
}
:deep(.v-window-item[value="contact"] .v-text-field .v-field__input) {
text-align: left !important;
direction: ltr !important;
}
:deep(.v-window-item[value="contact"] .v-text-field .v-label) {
text-align: right !important;
}
@media (max-width: 600px) {
.content-container {
max-height: calc(100vh - 120px);
}
.sticky-tabs {
-webkit-overflow-scrolling: touch;
}
}
</style>

View file

@ -137,6 +137,14 @@ export default {
try {
const response = await axios.get('/api/accounting/table/childs/cost');
this.treeItems = response.data;
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue;
} else {
this.selectedItem = this.findItemById(this.treeItems, this.modelValue);
}
}
} catch (error) {
console.error('خطا در دریافت داده‌ها:', error);
this.$toast.error('خطا در بارگذاری داده‌ها');

View file

@ -295,15 +295,7 @@
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-autocomplete
v-model="item.person"
:items="listPersons"
item-title="name"
item-value="id"
label="شخص"
variant="outlined"
density="compact"
></v-autocomplete>
<Hpersonsearch v-model="item.id" label="شخص" />
</v-col>
<v-col cols="12" md="4">
<Hnumberinput
@ -346,6 +338,7 @@ import archiveUpload from "../component/archive/archiveUpload.vue";
import Hdatepicker from "@/components/forms/Hdatepicker.vue";
import Hnumberinput from "@/components/forms/Hnumberinput.vue";
import Htabletreeselect from '@/components/forms/Htabletreeselect.vue'
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue'
// import the styles
import quickAdd from "../component/person/quickAdd.vue";
export default {
@ -357,6 +350,7 @@ export default {
Hdatepicker,
Hnumberinput,
Htabletreeselect,
Hpersonsearch,
},
data: () => {
return {
@ -486,7 +480,7 @@ export default {
},
addPerson() {
this.persons.push({
person: '',
id: '',
amount: '',
des: ''
})
@ -575,11 +569,6 @@ export default {
axios.post('/api/salary/list').then((response) => {
this.listSalarys = response.data;
});
//get list of persons
axios.post('/api/person/list/search').then((response) => {
this.listPersons = response.data;
});
},
save() {
let haszero = false;
@ -618,7 +607,7 @@ export default {
}
})
this.persons.forEach((item) => {
if (item.person == null || item.person == '') {
if (item.id == null || item.id == '') {
sideOK = false;
}
})
@ -697,7 +686,7 @@ export default {
this.persons.forEach((item) => {
if (item.des == '') item.des = 'هزینه'
rows.push({
id: item.person,
id: item.id,
bs: parseInt(item.amount),
bd: 0,
des: item.des,