more progress in hrm plugin
This commit is contained in:
parent
1418591120
commit
5334b1fddb
|
@ -281,13 +281,206 @@ class AttendanceController extends AbstractController
|
||||||
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
|
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// دریافت نوع پرسنل "کارمند" یا "employee"
|
$params = [];
|
||||||
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'employee']);
|
if ($content = $request->getContent()) {
|
||||||
|
$params = json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = $params['search'] ?? '';
|
||||||
|
$page = $params['page'] ?? 1;
|
||||||
|
$limit = $params['limit'] ?? 20;
|
||||||
|
|
||||||
|
// دریافت نوع پرسنل "کارمند" - کد صحیح در دیتابیس: emplyee
|
||||||
|
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
|
||||||
if (!$employeeType) {
|
if (!$employeeType) {
|
||||||
// اگر نوع "employee" وجود نداشت، نوع "کارمند" را جستجو کن
|
// اگر نوع "emplyee" وجود نداشت، نوع "کارمند" را جستجو کن
|
||||||
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']);
|
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
$qb->select('p')
|
||||||
|
->from(Person::class, 'p')
|
||||||
|
->where('p.bid = :bid')
|
||||||
|
->setParameter('bid', $acc['bid']);
|
||||||
|
|
||||||
|
// فقط پرسنلهایی که نوع پرسنل دارند
|
||||||
|
if ($employeeType) {
|
||||||
|
$qb->join('p.type', 't')
|
||||||
|
->andWhere('t = :employeeType')
|
||||||
|
->setParameter('employeeType', $employeeType);
|
||||||
|
} else {
|
||||||
|
// اگر نوع کارمند پیدا نشد، حداقل پرسنلهایی که نوع دارند را برگردان
|
||||||
|
$qb->join('p.type', 't');
|
||||||
|
}
|
||||||
|
|
||||||
|
// اعمال فیلتر جستجو
|
||||||
|
if (!empty($search)) {
|
||||||
|
$qb->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search')
|
||||||
|
->setParameter('search', '%' . $search . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->orderBy('p.nikename', 'ASC');
|
||||||
|
|
||||||
|
// محاسبه تعداد کل
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$totalCount = $countQb->select('COUNT(p.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
// اعمال صفحهبندی
|
||||||
|
$qb->setFirstResult(($page - 1) * $limit)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
$employees = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($employees as $employee) {
|
||||||
|
$result[] = [
|
||||||
|
'id' => $employee->getId(),
|
||||||
|
'name' => $employee->getNikename(),
|
||||||
|
'code' => $employee->getCode(),
|
||||||
|
'mobile' => $employee->getMobile(),
|
||||||
|
'tel' => $employee->getTel(),
|
||||||
|
'email' => $employee->getEmail(),
|
||||||
|
'company' => $employee->getCompany(),
|
||||||
|
'address' => $employee->getAddress(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'items' => $result,
|
||||||
|
'total' => $totalCount,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/hrm/attendance/employees/search', name: 'hrm_attendance_employees_search', methods: ['POST'])]
|
||||||
|
public function searchEmployees(Request $request, Access $access): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugHrmAttendance');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($content = $request->getContent()) {
|
||||||
|
$params = json_decode($content, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = $params['search'] ?? '';
|
||||||
|
$page = $params['page'] ?? 1;
|
||||||
|
$limit = $params['limit'] ?? 10;
|
||||||
|
|
||||||
|
// فیلترهای پیشرفته
|
||||||
|
$code = $params['code'] ?? '';
|
||||||
|
$mobile = $params['mobile'] ?? '';
|
||||||
|
$company = $params['company'] ?? '';
|
||||||
|
$email = $params['email'] ?? '';
|
||||||
|
|
||||||
|
// دریافت نوع پرسنل "کارمند" - کد صحیح در دیتابیس: emplyee
|
||||||
|
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
|
||||||
|
if (!$employeeType) {
|
||||||
|
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'کارمند']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
$qb->select('p')
|
||||||
|
->from(Person::class, 'p')
|
||||||
|
->where('p.bid = :bid')
|
||||||
|
->setParameter('bid', $acc['bid']);
|
||||||
|
|
||||||
|
// فقط پرسنلهایی که نوع پرسنل دارند
|
||||||
|
if ($employeeType) {
|
||||||
|
$qb->join('p.type', 't')
|
||||||
|
->andWhere('t = :employeeType')
|
||||||
|
->setParameter('employeeType', $employeeType);
|
||||||
|
} else {
|
||||||
|
// اگر نوع کارمند پیدا نشد، حداقل پرسنلهایی که نوع دارند را برگردان
|
||||||
|
$qb->join('p.type', 't');
|
||||||
|
}
|
||||||
|
|
||||||
|
// اعمال فیلتر جستجو عمومی
|
||||||
|
if (!empty($search)) {
|
||||||
|
$qb->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search')
|
||||||
|
->setParameter('search', '%' . $search . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// اعمال فیلترهای پیشرفته
|
||||||
|
if (!empty($code)) {
|
||||||
|
$qb->andWhere('p.code LIKE :code')
|
||||||
|
->setParameter('code', '%' . $code . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($mobile)) {
|
||||||
|
$qb->andWhere('p.mobile LIKE :mobile')
|
||||||
|
->setParameter('mobile', '%' . $mobile . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($company)) {
|
||||||
|
$qb->andWhere('p.company LIKE :company')
|
||||||
|
->setParameter('company', '%' . $company . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($email)) {
|
||||||
|
$qb->andWhere('p.email LIKE :email')
|
||||||
|
->setParameter('email', '%' . $email . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->orderBy('p.nikename', 'ASC');
|
||||||
|
|
||||||
|
// محاسبه تعداد کل
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$totalCount = $countQb->select('COUNT(p.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
// اعمال صفحهبندی
|
||||||
|
$qb->setFirstResult(($page - 1) * $limit)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
$employees = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($employees as $employee) {
|
||||||
|
$result[] = [
|
||||||
|
'id' => $employee->getId(),
|
||||||
|
'name' => $employee->getNikename(),
|
||||||
|
'code' => $employee->getCode(),
|
||||||
|
'mobile' => $employee->getMobile(),
|
||||||
|
'tel' => $employee->getTel(),
|
||||||
|
'email' => $employee->getEmail(),
|
||||||
|
'company' => $employee->getCompany(),
|
||||||
|
'address' => $employee->getAddress(),
|
||||||
|
'fullName' => $employee->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'items' => $result,
|
||||||
|
'total' => $totalCount,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/hrm/attendance/employees/test', name: 'hrm_attendance_employees_test', methods: ['GET'])]
|
||||||
|
public function testEmployees(Access $access): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$acc = $access->hasRole('plugHrmAttendance');
|
||||||
|
if (!$acc) {
|
||||||
|
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// دریافت نوع پرسنل "کارمند"
|
||||||
|
$employeeType = $this->entityManager->getRepository(PersonType::class)->findOneBy(['code' => 'emplyee']);
|
||||||
|
|
||||||
$qb = $this->entityManager->createQueryBuilder();
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
$qb->select('p')
|
$qb->select('p')
|
||||||
->from(Person::class, 'p')
|
->from(Person::class, 'p')
|
||||||
|
@ -298,10 +491,11 @@ class AttendanceController extends AbstractController
|
||||||
$qb->join('p.type', 't')
|
$qb->join('p.type', 't')
|
||||||
->andWhere('t = :employeeType')
|
->andWhere('t = :employeeType')
|
||||||
->setParameter('employeeType', $employeeType);
|
->setParameter('employeeType', $employeeType);
|
||||||
|
} else {
|
||||||
|
$qb->join('p.type', 't');
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb->orderBy('p.nikename', 'ASC');
|
$qb->setMaxResults(5);
|
||||||
|
|
||||||
$employees = $qb->getQuery()->getResult();
|
$employees = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
|
@ -310,10 +504,18 @@ class AttendanceController extends AbstractController
|
||||||
'id' => $employee->getId(),
|
'id' => $employee->getId(),
|
||||||
'name' => $employee->getNikename(),
|
'name' => $employee->getNikename(),
|
||||||
'code' => $employee->getCode(),
|
'code' => $employee->getCode(),
|
||||||
|
'bid' => $employee->getBid()->getId(),
|
||||||
|
'hasType' => $employee->getType()->count() > 0
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->json($result);
|
return $this->json([
|
||||||
|
'business_id' => $acc['bid'],
|
||||||
|
'employee_type_found' => $employeeType ? true : false,
|
||||||
|
'employee_type_code' => $employeeType ? $employeeType->getCode() : null,
|
||||||
|
'total_employees' => count($result),
|
||||||
|
'employees' => $result
|
||||||
|
]);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->json(['error' => $e->getMessage()], 500);
|
return $this->json(['error' => $e->getMessage()], 500);
|
||||||
|
|
469
webUI/src/components/forms/HemployeeAdvancedSearch.vue
Normal file
469
webUI/src/components/forms/HemployeeAdvancedSearch.vue
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu v-model="menu" :close-on-content-click="false" max-width="600">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-text-field
|
||||||
|
v-bind="props"
|
||||||
|
:model-value="displayValue"
|
||||||
|
@update:model-value="updateDisplayValue"
|
||||||
|
variant="outlined"
|
||||||
|
:label="label"
|
||||||
|
class="my-0"
|
||||||
|
prepend-inner-icon="mdi-account-search"
|
||||||
|
clearable
|
||||||
|
@click:clear="clearSelection"
|
||||||
|
:loading="loading"
|
||||||
|
@keydown.enter="handleEnter"
|
||||||
|
density="comfortable"
|
||||||
|
style="font-size: 0.7rem;"
|
||||||
|
:error="hasError"
|
||||||
|
:error-messages="errorMessage"
|
||||||
|
>
|
||||||
|
<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="500" max-width="600" class="search-card">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
|
<span class="text-h6">جستجوی پرسنل</span>
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="menu = false" />
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<!-- فیلترهای پیشرفته -->
|
||||||
|
<v-expansion-panels variant="accordion" class="mb-4">
|
||||||
|
<v-expansion-panel>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<v-icon start>mdi-filter-variant</v-icon>
|
||||||
|
فیلترهای پیشرفته
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="advancedFilters.code"
|
||||||
|
label="کد پرسنل"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="advancedFilters.mobile"
|
||||||
|
label="شماره موبایل"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="advancedFilters.company"
|
||||||
|
label="شرکت"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="advancedFilters.email"
|
||||||
|
label="ایمیل"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" class="d-flex justify-end">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
@click="applyAdvancedFilters"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
اعمال فیلترها
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="ms-2"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
@click="clearAdvancedFilters"
|
||||||
|
>
|
||||||
|
پاک کردن
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
|
||||||
|
<!-- فیلد جستجو -->
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
label="جستجو در نام، کد، موبایل..."
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="searchEmployees"
|
||||||
|
@click:clear="searchEmployees"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- نتایج جستجو -->
|
||||||
|
<template v-if="!loading">
|
||||||
|
<v-list density="compact" class="list-container mt-4">
|
||||||
|
<template v-if="filteredItems.length > 0">
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in filteredItems"
|
||||||
|
:key="item.id"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
class="mb-2 search-result-item"
|
||||||
|
:class="{ 'selected-item': selectedItem?.id === item.id }"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar size="40" color="primary" class="text-white">
|
||||||
|
<span class="text-h6">{{ getInitials(item.name) }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title class="font-weight-medium">
|
||||||
|
{{ item.name }}
|
||||||
|
<span class="text-caption text-grey-darken-1 ms-2">({{ item.code }})</span>
|
||||||
|
</v-list-item-title>
|
||||||
|
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<div class="d-flex align-center mt-1">
|
||||||
|
<v-icon size="small" color="primary" class="mr-1">mdi-cellphone</v-icon>
|
||||||
|
<span class="text-caption">{{ item.mobile || 'بدون موبایل' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.company" class="d-flex align-center mt-1">
|
||||||
|
<v-icon size="small" color="secondary" class="mr-1">mdi-domain</v-icon>
|
||||||
|
<span class="text-caption">{{ item.company }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.email" class="d-flex align-center mt-1">
|
||||||
|
<v-icon size="small" color="info" class="mr-1">mdi-email</v-icon>
|
||||||
|
<span class="text-caption">{{ item.email }}</span>
|
||||||
|
</div>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-chip
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
class="employee-chip"
|
||||||
|
>
|
||||||
|
کارمند
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title class="text-center text-grey">
|
||||||
|
<v-icon size="large" color="grey" class="mb-2">mdi-account-search</v-icon>
|
||||||
|
<div>نتیجهای یافت نشد</div>
|
||||||
|
<div class="text-caption">لطفاً عبارت جستجو را تغییر دهید</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- صفحهبندی -->
|
||||||
|
<div v-if="totalItems > itemsPerPage" class="d-flex justify-center mt-4">
|
||||||
|
<v-pagination
|
||||||
|
v-model="currentPage"
|
||||||
|
:length="Math.ceil(totalItems / itemsPerPage)"
|
||||||
|
:total-visible="5"
|
||||||
|
@update:model-value="onPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-progress-circular
|
||||||
|
v-else
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
class="d-flex mx-auto my-8"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HemployeeAdvancedSearch',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [Object, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: 'جستجوی پرسنل'
|
||||||
|
},
|
||||||
|
returnObject: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedItem: null,
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
menu: false,
|
||||||
|
searchQuery: '',
|
||||||
|
totalItems: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
searchTimeout: null,
|
||||||
|
advancedFilters: {
|
||||||
|
code: '',
|
||||||
|
mobile: '',
|
||||||
|
company: '',
|
||||||
|
email: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredItems() {
|
||||||
|
return Array.isArray(this.items) ? this.items : [];
|
||||||
|
},
|
||||||
|
displayValue() {
|
||||||
|
if (this.menu) {
|
||||||
|
return this.searchQuery;
|
||||||
|
}
|
||||||
|
return this.selectedItem ? this.selectedItem.name : '';
|
||||||
|
},
|
||||||
|
hasError() {
|
||||||
|
if (this.returnObject) {
|
||||||
|
return !(this.modelValue && typeof this.modelValue === 'object' && this.modelValue.id);
|
||||||
|
} else {
|
||||||
|
return !(this.modelValue && typeof this.modelValue === 'number');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage() {
|
||||||
|
if (this.hasError) {
|
||||||
|
return 'انتخاب پرسنل الزامی است';
|
||||||
|
}
|
||||||
|
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.searchEmployees();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.searchEmployees();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateDisplayValue(value) {
|
||||||
|
this.searchQuery = value;
|
||||||
|
if (!value) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async searchEmployees() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
search: this.searchQuery,
|
||||||
|
page: this.currentPage,
|
||||||
|
limit: this.itemsPerPage,
|
||||||
|
...this.advancedFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post('/api/hrm/attendance/employees/search', params);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.items = [];
|
||||||
|
this.totalItems = 0;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyAdvancedFilters() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.searchEmployees();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAdvancedFilters() {
|
||||||
|
this.advancedFilters = {
|
||||||
|
code: '',
|
||||||
|
mobile: '',
|
||||||
|
company: '',
|
||||||
|
email: ''
|
||||||
|
};
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.searchEmployees();
|
||||||
|
},
|
||||||
|
|
||||||
|
onPageChange(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.searchEmployees();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectItem(item) {
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.menu = false;
|
||||||
|
|
||||||
|
if (this.returnObject) {
|
||||||
|
this.$emit('update:modelValue', item);
|
||||||
|
} else {
|
||||||
|
this.$emit('update:modelValue', item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change', item);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedItem = null;
|
||||||
|
this.searchQuery = '';
|
||||||
|
|
||||||
|
if (this.returnObject) {
|
||||||
|
this.$emit('update:modelValue', null);
|
||||||
|
} else {
|
||||||
|
this.$emit('update:modelValue', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEnter() {
|
||||||
|
if (this.filteredItems.length === 1) {
|
||||||
|
this.selectItem(this.filteredItems[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitials(name) {
|
||||||
|
if (!name) return '?';
|
||||||
|
return name.split(' ').map(word => word.charAt(0)).join('').toUpperCase().substring(0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(var(--v-theme-outline), 0.1);
|
||||||
|
background-color: rgb(var(--v-theme-surface));
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:hover {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-chip {
|
||||||
|
min-width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list-item__content) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list-item) {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-card) {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-card-text) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.text-primary-dark) {
|
||||||
|
color: rgb(var(--v-theme-primary-dark)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-expansion-panel) {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-expansion-panel-title) {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
304
webUI/src/components/forms/HemployeeSearch.vue
Normal file
304
webUI/src/components/forms/HemployeeSearch.vue
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
<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"
|
||||||
|
:error-messages="errorMessages"
|
||||||
|
:rules="combinedRules"
|
||||||
|
:label="label"
|
||||||
|
class="my-0"
|
||||||
|
prepend-inner-icon="mdi-account-tie"
|
||||||
|
clearable
|
||||||
|
@click:clear="clearSelection"
|
||||||
|
:loading="loading"
|
||||||
|
@keydown.enter="handleEnter"
|
||||||
|
hide-details
|
||||||
|
density="comfortable"
|
||||||
|
style="font-size: 0.7rem;"
|
||||||
|
>
|
||||||
|
<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" class="search-card">
|
||||||
|
<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-2 search-result-item"
|
||||||
|
:class="{ 'selected-item': selectedItem?.id === item.id }"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column w-100">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon size="small" color="primary" class="mr-1">mdi-account-tie</v-icon>
|
||||||
|
<span class="text-caption text-primary-dark">{{ item.mobile || 'بدون موبایل' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-caption text-grey-darken-1">{{ item.code }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<span class="text-body-2 font-weight-medium text-primary-dark">{{ item.name }}</span>
|
||||||
|
<v-chip
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
class="employee-chip"
|
||||||
|
>
|
||||||
|
کارمند
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HemployeeSearch',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [Object, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: 'پرسنل'
|
||||||
|
},
|
||||||
|
returnObject: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedItem: null,
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
menu: false,
|
||||||
|
searchQuery: '',
|
||||||
|
totalItems: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
searchTimeout: null,
|
||||||
|
errorMessages: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredItems() {
|
||||||
|
return Array.isArray(this.items) ? this.items : [];
|
||||||
|
},
|
||||||
|
displayValue: {
|
||||||
|
get() {
|
||||||
|
if (this.menu) {
|
||||||
|
return this.searchQuery;
|
||||||
|
}
|
||||||
|
return this.selectedItem ? this.selectedItem.name : this.searchQuery;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.searchQuery = value;
|
||||||
|
if (!value) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
combinedRules() {
|
||||||
|
return [
|
||||||
|
v => !!v || 'انتخاب پرسنل الزامی است',
|
||||||
|
...this.rules
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/hrm/attendance/employees/search', {
|
||||||
|
search: this.searchQuery,
|
||||||
|
page: this.currentPage,
|
||||||
|
limit: this.itemsPerPage
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.items = [];
|
||||||
|
this.totalItems = 0;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectItem(item) {
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.menu = false;
|
||||||
|
|
||||||
|
if (this.returnObject) {
|
||||||
|
this.$emit('update:modelValue', item);
|
||||||
|
} else {
|
||||||
|
this.$emit('update:modelValue', item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change', item);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedItem = null;
|
||||||
|
this.searchQuery = '';
|
||||||
|
|
||||||
|
if (this.returnObject) {
|
||||||
|
this.$emit('update:modelValue', null);
|
||||||
|
} else {
|
||||||
|
this.$emit('update:modelValue', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEnter() {
|
||||||
|
if (this.filteredItems.length === 1) {
|
||||||
|
this.selectItem(this.filteredItems[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(var(--v-theme-outline), 0.1);
|
||||||
|
background-color: rgb(var(--v-theme-surface));
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:hover {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-chip {
|
||||||
|
min-width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list-item__content) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list-item) {
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-list) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-card) {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-card-text) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.text-primary-dark) {
|
||||||
|
color: rgb(var(--v-theme-primary-dark)) !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,6 +8,10 @@ const en_lang = {
|
||||||
logout_loading: "you logged out ..."
|
logout_loading: "you logged out ..."
|
||||||
},
|
},
|
||||||
dialog:{
|
dialog:{
|
||||||
|
back: "Back",
|
||||||
|
add_new: "Add New",
|
||||||
|
delete: "Delete",
|
||||||
|
manage_columns: "Manage Columns",
|
||||||
ok: "Ok",
|
ok: "Ok",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
|
@ -89,6 +93,12 @@ const en_lang = {
|
||||||
inquiry: "Inquiries",
|
inquiry: "Inquiries",
|
||||||
hrm: 'HR & Payroll',
|
hrm: 'HR & Payroll',
|
||||||
hrm_docs: 'Payroll Document',
|
hrm_docs: 'Payroll Document',
|
||||||
|
hrm_attendance: 'Personnel Attendance',
|
||||||
|
hrm_attendance_list: 'Attendance List',
|
||||||
|
hrm_attendance_add: 'Add Attendance',
|
||||||
|
hrm_attendance_edit: 'Edit Attendance',
|
||||||
|
hrm_attendance_view: 'View Attendance',
|
||||||
|
hrm_attendance_reports: 'Attendance Reports',
|
||||||
warranty_system: 'Warranty System',
|
warranty_system: 'Warranty System',
|
||||||
warranty_serials: 'Warranty Serials',
|
warranty_serials: 'Warranty Serials',
|
||||||
business_switcher: 'Switch Business',
|
business_switcher: 'Switch Business',
|
||||||
|
|
|
@ -40,6 +40,7 @@ const fa_lang = {
|
||||||
credit_balance: "تراز بستانکار",
|
credit_balance: "تراز بستانکار",
|
||||||
operations: "عملیات",
|
operations: "عملیات",
|
||||||
rows_per_page: "تعداد سطر در هر صفحه",
|
rows_per_page: "تعداد سطر در هر صفحه",
|
||||||
|
"تعداد سطر": "تعداد سطر",
|
||||||
no_data: "اطلاعاتی برای نمایش وجود ندارد",
|
no_data: "اطلاعاتی برای نمایش وجود ندارد",
|
||||||
of: "از",
|
of: "از",
|
||||||
date: "تاریخ",
|
date: "تاریخ",
|
||||||
|
@ -308,6 +309,10 @@ const fa_lang = {
|
||||||
fetch_data_error: "خطا در گرفتن داده از {url}"
|
fetch_data_error: "خطا در گرفتن داده از {url}"
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
back: "بازگشت",
|
||||||
|
add_new: "افزودن جدید",
|
||||||
|
delete: "حذف",
|
||||||
|
manage_columns: "مدیریت ستونها",
|
||||||
person_with_det_report: 'گزارش تفضیلی اشخاص',
|
person_with_det_report: 'گزارش تفضیلی اشخاص',
|
||||||
change_password_label: 'تغییر کلمه عبور',
|
change_password_label: 'تغییر کلمه عبور',
|
||||||
download: 'دانلود',
|
download: 'دانلود',
|
||||||
|
@ -406,7 +411,6 @@ const fa_lang = {
|
||||||
filter: "فیلتر",
|
filter: "فیلتر",
|
||||||
filters: "فیلترها",
|
filters: "فیلترها",
|
||||||
commodity_not_found: "کالا یافت نشد",
|
commodity_not_found: "کالا یافت نشد",
|
||||||
add_new: "افزودن مورد جدید",
|
|
||||||
fiscal_year: "سال مالی",
|
fiscal_year: "سال مالی",
|
||||||
currency: "واحد پولی",
|
currency: "واحد پولی",
|
||||||
notifications: "اعلانات",
|
notifications: "اعلانات",
|
||||||
|
@ -431,7 +435,6 @@ const fa_lang = {
|
||||||
payment: "ثبت پرداخت",
|
payment: "ثبت پرداخت",
|
||||||
exit: "خروج از حساب کاربری",
|
exit: "خروج از حساب کاربری",
|
||||||
complete_all: "موارد الزامی را تکمیل کنید",
|
complete_all: "موارد الزامی را تکمیل کنید",
|
||||||
back: "صفحه قبل",
|
|
||||||
search: "جست و جو ...",
|
search: "جست و جو ...",
|
||||||
general: "عمومی",
|
general: "عمومی",
|
||||||
prices: "قیمتها",
|
prices: "قیمتها",
|
||||||
|
@ -488,7 +491,6 @@ const fa_lang = {
|
||||||
system: "سیستم",
|
system: "سیستم",
|
||||||
database: "بانک اطلاعاتی",
|
database: "بانک اطلاعاتی",
|
||||||
edit: "ویرایش",
|
edit: "ویرایش",
|
||||||
delete: "حذف",
|
|
||||||
each: "هر",
|
each: "هر",
|
||||||
logout: "خروج",
|
logout: "خروج",
|
||||||
import_excel: "درون ریزی از اکسل",
|
import_excel: "درون ریزی از اکسل",
|
||||||
|
@ -519,12 +521,18 @@ const fa_lang = {
|
||||||
error_operation: "در انجام عملیات خطایی به وجود آمد.در صورت تکرار خطا با پشتیبان نرم افزار تماس بگیرید.",
|
error_operation: "در انجام عملیات خطایی به وجود آمد.در صورت تکرار خطا با پشتیبان نرم افزار تماس بگیرید.",
|
||||||
"success": "موفقیت",
|
"success": "موفقیت",
|
||||||
"error_unknown": "خطای ناشناختهای رخ داد",
|
"error_unknown": "خطای ناشناختهای رخ داد",
|
||||||
"manage_columns": "مدیریت ستونها",
|
|
||||||
customize_columns: "شخصیسازی ستونها",
|
customize_columns: "شخصیسازی ستونها",
|
||||||
close_dialog: "بستن",
|
close_dialog: "بستن",
|
||||||
presell_info: "اطلاعات پیش فاکتور",
|
presell_info: "اطلاعات پیش فاکتور",
|
||||||
financial_info: "اطلاعات مالی",
|
financial_info: "اطلاعات مالی",
|
||||||
invoice_items: "اقلام فاکتور",
|
invoice_items: "اقلام فاکتور",
|
||||||
|
select_file: "انتخاب فایل",
|
||||||
|
select_file_first: "لطفاً فایل را انتخاب کنید",
|
||||||
|
file_format: "فرمت فایل",
|
||||||
|
clear: "پاک کردن",
|
||||||
|
yes: "بله",
|
||||||
|
no: "خیر",
|
||||||
|
no_items_selected: "هیچ موردی انتخاب نشده است",
|
||||||
hrm: {
|
hrm: {
|
||||||
title: "سند حقوق",
|
title: "سند حقوق",
|
||||||
date: "تاریخ",
|
date: "تاریخ",
|
||||||
|
@ -549,6 +557,72 @@ const fa_lang = {
|
||||||
date: "تاریخ الزامی است",
|
date: "تاریخ الزامی است",
|
||||||
description: "توضیحات الزامی است",
|
description: "توضیحات الزامی است",
|
||||||
person: "انتخاب شخص الزامی است"
|
person: "انتخاب شخص الزامی است"
|
||||||
|
},
|
||||||
|
attendance: {
|
||||||
|
title: "تردد پرسنل",
|
||||||
|
list: "لیست ترددها",
|
||||||
|
add: "افزودن تردد",
|
||||||
|
edit: "ویرایش تردد",
|
||||||
|
view: "مشاهده تردد",
|
||||||
|
reports: "گزارشات تردد",
|
||||||
|
employee: "پرسنل",
|
||||||
|
date: "تاریخ",
|
||||||
|
time: "زمان",
|
||||||
|
type: "نوع",
|
||||||
|
entry: "ورود",
|
||||||
|
exit: "خروج",
|
||||||
|
total_hours: "ساعات کل کار",
|
||||||
|
overtime_hours: "ساعات اضافهکاری",
|
||||||
|
description: "توضیحات",
|
||||||
|
status: "وضعیت",
|
||||||
|
actions: "عملیات",
|
||||||
|
created_at: "تاریخ ثبت",
|
||||||
|
person_name: "نام پرسنل",
|
||||||
|
person_code: "کد پرسنل",
|
||||||
|
work_hours: "ساعات کار",
|
||||||
|
overtime: "اضافهکاری",
|
||||||
|
notes: "یادداشت",
|
||||||
|
import_excel: "واردات از اکسل",
|
||||||
|
export_excel: "خروجی اکسل",
|
||||||
|
generate_report: "تولید گزارش",
|
||||||
|
daily_report: "گزارش روزانه",
|
||||||
|
monthly_report: "گزارش ماهانه",
|
||||||
|
overtime_report: "گزارش اضافهکاری",
|
||||||
|
absence_report: "گزارش تاخیر و غیبت",
|
||||||
|
from_date: "از تاریخ",
|
||||||
|
to_date: "تا تاریخ",
|
||||||
|
filter_by_person: "فیلتر بر اساس پرسنل",
|
||||||
|
all_persons: "همه پرسنل",
|
||||||
|
report_filters: "فیلترهای گزارش",
|
||||||
|
report_type: "نوع گزارش",
|
||||||
|
summary: {
|
||||||
|
total_days: "کل روزهای کاری",
|
||||||
|
total_hours: "کل ساعات کار",
|
||||||
|
total_overtime: "کل اضافهکاری",
|
||||||
|
average_hours: "میانگین ساعات روزانه"
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
full_attendance: "حضور کامل",
|
||||||
|
part_time: "نیمه وقت",
|
||||||
|
late: "تاخیر",
|
||||||
|
absent: "غیبت"
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
no_data: "هیچ دادهای برای نمایش وجود ندارد",
|
||||||
|
select_date_range: "لطفاً بازه زمانی را مشخص کنید",
|
||||||
|
import_success: "رکوردها با موفقیت وارد شدند",
|
||||||
|
export_success: "فایل با موفقیت دانلود شد",
|
||||||
|
delete_confirm: "آیا برای حذف موارد انتخاب شده مطمئن هستید؟",
|
||||||
|
delete_success: "موارد انتخاب شده با موفقیت حذف شدند",
|
||||||
|
save_success: "تردد با موفقیت ذخیره شد",
|
||||||
|
edit_success: "تردد با موفقیت ویرایش شد",
|
||||||
|
load_error: "خطا در بارگذاری اطلاعات",
|
||||||
|
save_error: "خطا در ذخیره اطلاعات",
|
||||||
|
delete_error: "خطا در حذف اطلاعات",
|
||||||
|
import_error: "خطا در واردات فایل",
|
||||||
|
export_error: "خطا در خروجی فایل",
|
||||||
|
report_error: "خطا در تولید گزارش"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
buysell_report: {
|
buysell_report: {
|
||||||
|
|
|
@ -1,23 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-toolbar color="primary" dark>
|
<v-toolbar color="toolbar" title="تردد پرسنل">
|
||||||
<v-toolbar-title>
|
<template v-slot:prepend>
|
||||||
<v-icon start icon="mdi-clock-check"></v-icon>
|
<v-tooltip :text="$t('dialog.back')" location="bottom">
|
||||||
مدیریت تردد پرسنل
|
<template v-slot:activator="{ props }">
|
||||||
</v-toolbar-title>
|
<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-spacer></v-spacer>
|
||||||
<v-btn v-if="permissions.plugHrmAttendance" prepend-icon="mdi-plus" @click="$router.push('/acc/hrm/attendance/mod/0')">
|
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
|
||||||
افزودن تردد
|
<template v-slot:activator="{ props }">
|
||||||
</v-btn>
|
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/hrm/attendance/mod/0" v-if="permissions.plugHrmAttendance"></v-btn>
|
||||||
<v-btn prepend-icon="mdi-upload" @click="showImportDialog = true">
|
</template>
|
||||||
واردات از اکسل
|
</v-tooltip>
|
||||||
</v-btn>
|
<v-tooltip :text="$t('dialog.delete')" location="bottom">
|
||||||
<v-btn prepend-icon="mdi-download" @click="exportData">
|
<template v-slot:activator="{ props }">
|
||||||
خروجی اکسل
|
<v-btn v-bind="props" icon="mdi-delete" color="danger" @click="deleteItems()" :disabled="itemsSelected.length === 0"></v-btn>
|
||||||
</v-btn>
|
</template>
|
||||||
<v-btn prepend-icon="mdi-chart-line" @click="$router.push('/acc/hrm/attendance/reports')">
|
</v-tooltip>
|
||||||
گزارشات
|
<v-tooltip text="واردات از اکسل" location="bottom">
|
||||||
</v-btn>
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-upload" color="success" @click="showImportDialog = true"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip text="خروجی اکسل" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-download" color="info" @click="exportData"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip text="گزارشات تردد" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-chart-line" color="warning" @click="$router.push('/acc/hrm/attendance/reports')"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<!-- دکمه تنظیمات ستونها -->
|
||||||
|
<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-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
|
@ -32,14 +55,11 @@
|
||||||
<Hdatepicker v-model="filters.toDate" label="تا تاریخ" />
|
<Hdatepicker v-model="filters.toDate" label="تا تاریخ" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-select
|
<HemployeeAdvancedSearch
|
||||||
v-model="filters.personId"
|
v-model="filters.personId"
|
||||||
:items="employees"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
label="پرسنل"
|
label="پرسنل"
|
||||||
clearable
|
return-object
|
||||||
prepend-inner-icon="mdi-account"
|
@change="onEmployeeChange"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="3" class="d-flex align-center">
|
<v-col cols="12" md="3" class="d-flex align-center">
|
||||||
|
@ -59,7 +79,7 @@
|
||||||
<!-- جدول -->
|
<!-- جدول -->
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
:headers="headers"
|
:headers="visibleHeaders"
|
||||||
:items="attendances"
|
:items="attendances"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:items-per-page="filters.limit"
|
:items-per-page="filters.limit"
|
||||||
|
@ -67,6 +87,8 @@
|
||||||
:server-items-length="total"
|
:server-items-length="total"
|
||||||
@update:options="handleTableUpdate"
|
@update:options="handleTableUpdate"
|
||||||
class="elevation-1"
|
class="elevation-1"
|
||||||
|
v-model="itemsSelected"
|
||||||
|
show-select
|
||||||
>
|
>
|
||||||
<template v-slot:item.date="{ item }">
|
<template v-slot:item.date="{ item }">
|
||||||
<span>{{ formatDate(item.date) }}</span>
|
<span>{{ formatDate(item.date) }}</span>
|
||||||
|
@ -120,10 +142,10 @@
|
||||||
/>
|
/>
|
||||||
<v-alert type="info" variant="tonal" class="mt-2">
|
<v-alert type="info" variant="tonal" class="mt-2">
|
||||||
<strong>فرمت فایل:</strong><br>
|
<strong>فرمت فایل:</strong><br>
|
||||||
ستون A: کد پرسنل<br>
|
کد پرسنل: A<br>
|
||||||
ستون B: تاریخ (YYYY/MM/DD)<br>
|
تاریخ: B (YYYY/MM/DD)<br>
|
||||||
ستون C: زمان (HH:MM)<br>
|
زمان: C (HH:MM)<br>
|
||||||
ستون D: نوع (ورود/خروج)
|
نوع: D (ورود/خروج)
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
|
@ -136,6 +158,27 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- دیالوگ تنظیمات ستونها -->
|
||||||
|
<v-dialog v-model="showColumnDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-toolbar-title>{{ $t('dialog.manage_columns') }}</v-toolbar-title>
|
||||||
|
<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 headers" :key="header.value" cols="12" sm="6" class="my-0 py-0">
|
||||||
|
<v-checkbox v-model="header.visible" :label="getHeaderTitle(header.value)" @update:model-value="updateColumnVisibility"
|
||||||
|
hide-details="auto" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<!-- اسنکبار -->
|
<!-- اسنکبار -->
|
||||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||||
{{ snackbar.text }}
|
{{ snackbar.text }}
|
||||||
|
@ -145,12 +188,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
||||||
|
import HemployeeAdvancedSearch from '@/components/forms/HemployeeAdvancedSearch.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AttendanceList',
|
name: 'AttendanceList',
|
||||||
components: {
|
components: {
|
||||||
Hdatepicker
|
Hdatepicker,
|
||||||
|
HemployeeAdvancedSearch
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -159,9 +205,10 @@ export default {
|
||||||
showImportDialog: false,
|
showImportDialog: false,
|
||||||
importFile: null,
|
importFile: null,
|
||||||
attendances: [],
|
attendances: [],
|
||||||
employees: [],
|
|
||||||
total: 0,
|
total: 0,
|
||||||
permissions: {},
|
permissions: {},
|
||||||
|
itemsSelected: [],
|
||||||
|
showColumnDialog: false,
|
||||||
filters: {
|
filters: {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
|
@ -170,13 +217,13 @@ export default {
|
||||||
personId: null
|
personId: null
|
||||||
},
|
},
|
||||||
headers: [
|
headers: [
|
||||||
{ title: 'تاریخ', value: 'date', sortable: true },
|
{ title: 'تاریخ', value: 'date', sortable: true, visible: true },
|
||||||
{ title: 'نام پرسنل', value: 'personName', sortable: true },
|
{ title: 'نام پرسنل', value: 'personName', sortable: true, visible: true },
|
||||||
{ title: 'ساعات کل کار', value: 'totalHours', sortable: true },
|
{ title: 'ساعات کل کار', value: 'totalHours', sortable: true, visible: true },
|
||||||
{ title: 'ساعات اضافهکاری', value: 'overtimeHours', sortable: true },
|
{ title: 'ساعات اضافهکاری', value: 'overtimeHours', sortable: true, visible: true },
|
||||||
{ title: 'توضیحات', value: 'description', sortable: false },
|
{ title: 'توضیحات', value: 'description', sortable: false, visible: true },
|
||||||
{ title: 'تاریخ ثبت', value: 'createdAt', sortable: true },
|
{ title: 'تاریخ ثبت', value: 'createdAt', sortable: true, visible: true },
|
||||||
{ title: 'عملیات', value: 'actions', sortable: false, width: '120px' }
|
{ title: 'عملیات', value: 'actions', sortable: false, visible: true, width: '120px' }
|
||||||
],
|
],
|
||||||
snackbar: {
|
snackbar: {
|
||||||
show: false,
|
show: false,
|
||||||
|
@ -185,10 +232,18 @@ export default {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
visibleHeaders() {
|
||||||
|
return this.headers.filter(header => header.visible).map(header => ({
|
||||||
|
...header,
|
||||||
|
title: this.getHeaderTitle(header.value)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadPermissions();
|
await this.loadPermissions();
|
||||||
await this.loadEmployees();
|
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
|
this.loadColumnSettings();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadPermissions() {
|
async loadPermissions() {
|
||||||
|
@ -200,15 +255,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadEmployees() {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/hrm/attendance/employees');
|
|
||||||
this.employees = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading employees:', error);
|
|
||||||
this.showSnackbar('خطا در بارگذاری لیست پرسنل', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
@ -292,6 +339,65 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteItems() {
|
||||||
|
if (this.itemsSelected.length === 0) {
|
||||||
|
this.showSnackbar('هیچ موردی انتخاب نشده است.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
text: this.$t('hrm.attendance.messages.delete_confirm'),
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: this.$t('dialog.yes'),
|
||||||
|
cancelButtonText: this.$t('dialog.no'),
|
||||||
|
icon: 'warning'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
this.loading = true;
|
||||||
|
const selectedIds = this.itemsSelected;
|
||||||
|
|
||||||
|
Promise.all(selectedIds.map(id =>
|
||||||
|
axios.post('/api/hrm/attendance/delete', { id })
|
||||||
|
)).then(() => {
|
||||||
|
this.showSnackbar('موارد انتخاب شده با موفقیت حذف شدند', 'success');
|
||||||
|
this.itemsSelected = [];
|
||||||
|
this.loadData();
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Error deleting items:', error);
|
||||||
|
this.showSnackbar('خطا در حذف موارد انتخاب شده', 'error');
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAttendance(item) {
|
||||||
|
Swal.fire({
|
||||||
|
text: 'آیا برای حذف این تردد مطمئن هستید؟',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'بله',
|
||||||
|
cancelButtonText: 'خیر',
|
||||||
|
icon: 'warning'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
this.loading = true;
|
||||||
|
axios.post('/api/hrm/attendance/delete', { id: item.id })
|
||||||
|
.then(() => {
|
||||||
|
this.showSnackbar('تردد با موفقیت حذف شد', 'success');
|
||||||
|
this.loadData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error deleting attendance:', error);
|
||||||
|
this.showSnackbar('خطا در حذف تردد', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleTableUpdate(options) {
|
handleTableUpdate(options) {
|
||||||
this.filters.page = options.page;
|
this.filters.page = options.page;
|
||||||
this.filters.limit = options.itemsPerPage;
|
this.filters.limit = options.itemsPerPage;
|
||||||
|
@ -327,6 +433,42 @@ export default {
|
||||||
text,
|
text,
|
||||||
color
|
color
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateColumnVisibility() {
|
||||||
|
localStorage.setItem('hrmAttendanceColumns', JSON.stringify(this.headers));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadColumnSettings() {
|
||||||
|
const savedColumns = localStorage.getItem('hrmAttendanceColumns');
|
||||||
|
if (savedColumns) {
|
||||||
|
const parsedColumns = JSON.parse(savedColumns);
|
||||||
|
this.headers = this.headers.map(header => ({
|
||||||
|
...header,
|
||||||
|
visible: parsedColumns.find(h => h.value === header.value)?.visible ?? true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getHeaderTitle(value) {
|
||||||
|
const titleMap = {
|
||||||
|
'date': 'تاریخ',
|
||||||
|
'personName': 'نام پرسنل',
|
||||||
|
'totalHours': 'ساعات کل کار',
|
||||||
|
'overtimeHours': 'ساعات اضافهکاری',
|
||||||
|
'description': 'توضیحات',
|
||||||
|
'createdAt': 'تاریخ ثبت',
|
||||||
|
'actions': 'عملیات'
|
||||||
|
};
|
||||||
|
return titleMap[value] || value;
|
||||||
|
},
|
||||||
|
|
||||||
|
onEmployeeChange(employee) {
|
||||||
|
if (employee) {
|
||||||
|
this.filters.personId = employee.id;
|
||||||
|
} else {
|
||||||
|
this.filters.personId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-toolbar color="primary" dark>
|
<v-toolbar color="toolbar" :title="isEdit ? 'ویرایش تردد' : 'افزودن تردد جدید'">
|
||||||
<v-toolbar-title>
|
<template v-slot:prepend>
|
||||||
<v-icon start icon="mdi-clock-check"></v-icon>
|
<v-tooltip :text="$t('dialog.back')" location="bottom">
|
||||||
{{ isEdit ? 'ویرایش تردد' : 'افزودن تردد جدید' }}
|
<template v-slot:activator="{ props }">
|
||||||
</v-toolbar-title>
|
<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-spacer></v-spacer>
|
||||||
<v-btn prepend-icon="mdi-arrow-left" @click="$router.push('/acc/hrm/attendance/list')">
|
<v-tooltip :text="isEdit ? 'ویرایش' : 'ذخیره'" location="bottom">
|
||||||
بازگشت
|
<template v-slot:activator="{ props }">
|
||||||
</v-btn>
|
<v-btn v-bind="props" icon="mdi-content-save" color="primary" @click="save" :loading="saving"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
|
@ -17,15 +23,11 @@
|
||||||
<v-form ref="form" @submit.prevent="save">
|
<v-form ref="form" @submit.prevent="save">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
<v-select
|
<HemployeeAdvancedSearch
|
||||||
v-model="form.personId"
|
v-model="selectedEmployee"
|
||||||
:items="employees"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
label="پرسنل *"
|
label="پرسنل *"
|
||||||
required
|
return-object
|
||||||
prepend-inner-icon="mdi-account"
|
@change="onEmployeeChange"
|
||||||
:rules="[v => !!v || 'انتخاب پرسنل الزامی است']"
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="4">
|
||||||
|
@ -93,7 +95,7 @@
|
||||||
color="error"
|
color="error"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="removeItem(index)"
|
@click="removeItem(index)"
|
||||||
:disabled="form.items.length <= 1"
|
:disabled="form.items.length === 1"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -103,29 +105,13 @@
|
||||||
<!-- خلاصه -->
|
<!-- خلاصه -->
|
||||||
<v-card class="mt-4" variant="outlined">
|
<v-card class="mt-4" variant="outlined">
|
||||||
<v-card-title class="text-h6">
|
<v-card-title class="text-h6">
|
||||||
<v-icon start>mdi-calculator</v-icon>
|
<v-icon start>mdi-chart-line</v-icon>
|
||||||
خلاصه ساعات کار
|
خلاصه
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-card variant="tonal" color="primary">
|
<v-card variant="tonal" color="primary">
|
||||||
<v-card-text class="text-center">
|
|
||||||
<div class="text-h6">{{ formatMinutesToHours(summary.totalHours) }}</div>
|
|
||||||
<div class="text-caption">ساعات کل کار</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-card variant="tonal" color="warning">
|
|
||||||
<v-card-text class="text-center">
|
|
||||||
<div class="text-h6">{{ formatMinutesToHours(summary.overtimeHours) }}</div>
|
|
||||||
<div class="text-caption">ساعات اضافهکاری</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-card variant="tonal" color="success">
|
|
||||||
<v-card-text class="text-center">
|
<v-card-text class="text-center">
|
||||||
<div class="text-h6">{{ summary.entries }}</div>
|
<div class="text-h6">{{ summary.entries }}</div>
|
||||||
<div class="text-caption">تعداد ورود</div>
|
<div class="text-caption">تعداد ورود</div>
|
||||||
|
@ -133,35 +119,48 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-card variant="tonal" color="info">
|
<v-card variant="tonal" color="secondary">
|
||||||
<v-card-text class="text-center">
|
<v-card-text class="text-center">
|
||||||
<div class="text-h6">{{ summary.exits }}</div>
|
<div class="text-h6">{{ summary.exits }}</div>
|
||||||
<div class="text-caption">تعداد خروج</div>
|
<div class="text-caption">تعداد خروج</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card variant="tonal" color="success">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h6">{{ formatMinutesToHours(summary.totalHours) }}</div>
|
||||||
|
<div class="text-caption">ساعات کار</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card variant="tonal" color="warning">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="text-h6">{{ formatMinutesToHours(summary.overtimeHours) }}</div>
|
||||||
|
<div class="text-caption">اضافهکاری</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-row class="mt-4">
|
<v-row class="mt-4">
|
||||||
<v-col cols="12" class="text-center">
|
<v-col cols="12" class="d-flex justify-end">
|
||||||
<v-btn
|
<v-btn
|
||||||
type="submit"
|
@click="$router.back()"
|
||||||
color="primary"
|
class="me-2"
|
||||||
size="large"
|
|
||||||
:loading="saving"
|
|
||||||
prepend-icon="mdi-content-save"
|
|
||||||
>
|
|
||||||
{{ isEdit ? 'ویرایش' : 'ثبت' }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
class="ms-2"
|
|
||||||
size="large"
|
|
||||||
@click="$router.push('/acc/hrm/attendance/list')"
|
|
||||||
>
|
>
|
||||||
انصراف
|
انصراف
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="save"
|
||||||
|
:loading="saving"
|
||||||
|
>
|
||||||
|
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
||||||
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
@ -179,17 +178,19 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
|
||||||
|
import HemployeeAdvancedSearch from '@/components/forms/HemployeeAdvancedSearch.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AttendanceMod',
|
name: 'AttendanceMod',
|
||||||
components: {
|
components: {
|
||||||
Hdatepicker
|
Hdatepicker,
|
||||||
|
HemployeeAdvancedSearch
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
employees: [],
|
selectedEmployee: null,
|
||||||
form: {
|
form: {
|
||||||
personId: null,
|
personId: null,
|
||||||
date: '',
|
date: '',
|
||||||
|
@ -247,17 +248,16 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadEmployees();
|
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadEmployees() {
|
onEmployeeChange(employee) {
|
||||||
try {
|
if (employee) {
|
||||||
const response = await axios.post('/api/hrm/attendance/employees');
|
this.selectedEmployee = employee;
|
||||||
this.employees = response.data;
|
this.form.personId = employee.id;
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error('Error loading employees:', error);
|
this.selectedEmployee = null;
|
||||||
this.showSnackbar('خطا در بارگذاری لیست پرسنل', 'error');
|
this.form.personId = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -269,6 +269,18 @@ export default {
|
||||||
const response = await axios.post(`/api/hrm/attendance/get/${id}`);
|
const response = await axios.post(`/api/hrm/attendance/get/${id}`);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
|
// ابتدا پرسنل را پیدا کنیم
|
||||||
|
const employeeResponse = await axios.post('/api/hrm/attendance/employees/search', {
|
||||||
|
search: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
const employee = employeeResponse.data.items.find(emp => emp.id === data.personId);
|
||||||
|
if (employee) {
|
||||||
|
this.selectedEmployee = employee;
|
||||||
|
}
|
||||||
|
|
||||||
this.form.personId = data.personId;
|
this.form.personId = data.personId;
|
||||||
this.form.date = data.date;
|
this.form.date = data.date;
|
||||||
this.form.description = data.description;
|
this.form.description = data.description;
|
||||||
|
@ -302,6 +314,12 @@ export default {
|
||||||
const { valid } = await this.$refs.form.validate();
|
const { valid } = await this.$refs.form.validate();
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
|
// بررسی validation دستی برای پرسنل
|
||||||
|
if (!this.selectedEmployee || !this.selectedEmployee.id) {
|
||||||
|
this.showSnackbar('انتخاب پرسنل الزامی است', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
const id = this.$route.params.id;
|
const id = this.$route.params.id;
|
||||||
|
@ -341,13 +359,6 @@ export default {
|
||||||
return hours * 60 + minutes;
|
return hours * 60 + minutes;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimestamp(date, time) {
|
|
||||||
// تبدیل تاریخ و زمان شمسی به timestamp
|
|
||||||
const dateTime = `${date} ${time}`;
|
|
||||||
// اینجا باید از سرویس Jdate استفاده شود
|
|
||||||
return Math.floor(Date.now() / 1000); // فعلاً timestamp فعلی
|
|
||||||
},
|
|
||||||
|
|
||||||
formatMinutesToHours(minutes) {
|
formatMinutesToHours(minutes) {
|
||||||
if (!minutes) return '0:00';
|
if (!minutes) return '0:00';
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
@ -355,6 +366,13 @@ export default {
|
||||||
return `${hours}:${mins.toString().padStart(2, '0')}`;
|
return `${hours}:${mins.toString().padStart(2, '0')}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTimestamp(date, time) {
|
||||||
|
// تبدیل تاریخ و زمان به timestamp
|
||||||
|
const [year, month, day] = date.split('/').map(Number);
|
||||||
|
const [hour, minute] = time.split(':').map(Number);
|
||||||
|
return new Date(year, month - 1, day, hour, minute).getTime();
|
||||||
|
},
|
||||||
|
|
||||||
showSnackbar(text, color = 'success') {
|
showSnackbar(text, color = 'success') {
|
||||||
this.snackbar = {
|
this.snackbar = {
|
||||||
show: true,
|
show: true,
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-toolbar color="primary" dark>
|
<v-toolbar color="toolbar" :title="'گزارشات تردد پرسنل'">
|
||||||
<v-toolbar-title>
|
<template v-slot:prepend>
|
||||||
<v-icon start icon="mdi-chart-line"></v-icon>
|
<v-tooltip :text="$t('dialog.back')" location="bottom">
|
||||||
گزارشات تردد پرسنل
|
<template v-slot:activator="{ props }">
|
||||||
</v-toolbar-title>
|
<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-spacer></v-spacer>
|
||||||
<v-btn prepend-icon="mdi-arrow-right" @click="$router.back()">
|
<v-tooltip text="تولید گزارش" location="bottom">
|
||||||
بازگشت
|
<template v-slot:activator="{ props }">
|
||||||
</v-btn>
|
<v-btn v-bind="props" icon="mdi-chart-line" color="primary" @click="generateReport" :loading="loading"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip text="خروجی اکسل" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-download" color="success" @click="exportReport" :loading="exporting" :disabled="!reportData.length"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-toolbar color="primary" dark>
|
<v-toolbar color="toolbar" :title="'مشاهده جزئیات تردد'">
|
||||||
<v-toolbar-title>
|
<template v-slot:prepend>
|
||||||
<v-icon start icon="mdi-eye"></v-icon>
|
<v-tooltip :text="$t('dialog.back')" location="bottom">
|
||||||
مشاهده جزئیات تردد
|
<template v-slot:activator="{ props }">
|
||||||
</v-toolbar-title>
|
<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-spacer></v-spacer>
|
||||||
<v-btn v-if="permissions.plugHrmAttendance" prepend-icon="mdi-pencil" @click="$router.push(`/acc/hrm/attendance/mod/${attendance.id}`)">
|
<v-tooltip text="ویرایش" location="bottom" v-if="permissions.plugHrmAttendance">
|
||||||
ویرایش
|
<template v-slot:activator="{ props }">
|
||||||
</v-btn>
|
<v-btn v-bind="props" icon="mdi-pencil" color="primary" @click="$router.push(`/acc/hrm/attendance/mod/${attendance.id}`)"></v-btn>
|
||||||
<v-btn prepend-icon="mdi-arrow-right" @click="$router.back()">
|
</template>
|
||||||
بازگشت
|
</v-tooltip>
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-container fluid class="pa-4" v-if="!loading">
|
<v-container fluid class="pa-4" v-if="!loading">
|
||||||
|
|
Loading…
Reference in a new issue