bug fix in modules and start working on hrm plugin

This commit is contained in:
Hesabix 2025-05-16 17:47:08 +00:00
parent 78f296cdcf
commit cdbe6e3ae9
31 changed files with 2782 additions and 380 deletions

View file

@ -542,6 +542,7 @@ class BusinessController extends AbstractController
'plugAccproCloseYear' => true,
'plugAccproPresell' => true,
'plugRepservice' => true,
'plugHrmDocs' => true,
];
} elseif ($perm) {
$result = [
@ -583,6 +584,7 @@ class BusinessController extends AbstractController
'plugAccproCloseYear' => $perm->isPlugAccproCloseYear(),
'plugRepservice' => $perm->isPlugRepservice(),
'plugAccproPresell' => $perm->isPlugAccproPresell(),
'plugHrmDocs' => $perm->isPlugHrmDocs(),
];
}
return $this->json($result);
@ -650,6 +652,7 @@ class BusinessController extends AbstractController
$perm->setPlugAccproPresell($params['plugAccproPresell']);
$perm->setPlugAccproAccounting($params['plugAccproAccounting']);
$perm->setPlugRepservice($params['plugRepservice']);
$perm->setPlugHrmDocs($params['plugHrmDocs']);
$entityManager->persist($perm);
$entityManager->flush();
$log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);

View file

@ -269,10 +269,10 @@ class CostController extends AbstractController
->setParameter('today', $today);
break;
case 'custom':
if (isset($filters['dateFrom']) && isset($filters['dateTo'])) {
if (isset($filters['date']) && isset($filters['date']['from']) && isset($filters['date']['to'])) {
$queryBuilder->andWhere('d.date BETWEEN :dateFrom AND :dateTo')
->setParameter('dateFrom', $filters['dateFrom'])
->setParameter('dateTo', $filters['dateTo']);
->setParameter('dateFrom', $filters['date']['from'])
->setParameter('dateTo', $filters['date']['to']);
}
break;
}

View file

@ -123,6 +123,9 @@ class Permission
#[ORM\Column(nullable: true)]
private ?bool $plugAccproPresell = null;
#[ORM\Column(nullable: true)]
private ?bool $plugHrmDocs = null;
public function getId(): ?int
{
return $this->id;
@ -560,4 +563,16 @@ class Permission
return $this;
}
public function isPlugHrmDocs(): ?bool
{
return $this->plugHrmDocs;
}
public function setPlugHrmDocs(?bool $plugHrmDocs): static
{
$this->plugHrmDocs = $plugHrmDocs;
return $this;
}
}

View file

@ -20,6 +20,8 @@
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^13.1.0",
"@zxing/browser": "^0.1.5",
"@zxing/library": "^0.21.3",
"animate.css": "^4.1.1",
"apexcharts": "^4.6.0",
"axios": "^1.8.4",
@ -27,6 +29,7 @@
"date-fns-jalali": "^3.2.0-0",
"downloadjs": "^1.4.7",
"file-saver": "^2.0.5",
"html5-qrcode": "^2.3.8",
"jalali-moment": "^3.3.11",
"libphonenumber-js": "^1.12.7",
"lodash": "^4.17.21",
@ -42,6 +45,7 @@
"vue-loading-overlay": "^6.0.6",
"vue-media-upload": "^2.2.4",
"vue-persian-datetime-picker": "^2.10.4",
"vue-qrcode-reader": "^5.7.2",
"vue-router": "^4.5.0",
"vue-select": "^4.0.0-beta.6",
"vue-spinner": "^1.0.4",
@ -49,6 +53,7 @@
"vue3-easy-data-table": "^1.5.47",
"vue3-perfect-scrollbar": "^2.0.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-qrcode-reader": "^0.0.1",
"vue3-tel-input": "^1.0.4",
"vue3-treeselect": "^0.1.10",
"vue3-treeview": "^0.4.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

View file

@ -0,0 +1,471 @@
<template>
<div>
<v-menu location="bottom end" :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props">
<v-icon>mdi-apps</v-icon>
</v-btn>
</template>
<v-card min-width="300" class="shortcuts-menu">
<v-list>
<v-list-item>
<template v-slot:prepend>
<v-icon color="primary">mdi-apps</v-icon>
</template>
<v-list-item-title class="text-h6">دسترسیهای سریع</v-list-item-title>
<template v-slot:append>
<v-tooltip
location="top"
text="افزودن دسترسی سریع جدید"
>
<template v-slot:activator="{ props }">
<v-btn
icon
variant="text"
color="primary"
size="small"
v-bind="props"
@click="showAddDialog = true"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
</v-tooltip>
</template>
</v-list-item>
<v-divider class="my-0"></v-divider>
<template v-if="!customShortcuts || customShortcuts.length === 0">
<v-list-item>
<v-list-item-title class="text-center py-4">
<v-icon
size="32"
color="grey-lighten-1"
class="mb-2 d-block mx-auto"
>
mdi-star-outline
</v-icon>
<span class="text-body-2 text-grey">
هیچ دسترسی سریعی تعریف نشده است
</span>
</v-list-item-title>
</v-list-item>
</template>
<template v-else>
<v-list-item
v-for="(shortcut, index) in customShortcuts"
:key="index"
class="shortcut-item"
@click="navigateTo(shortcut.path)"
>
<template v-slot:prepend>
<v-avatar
:color="getRandomColor(index)"
size="32"
class="shortcut-icon"
>
<v-icon :icon="shortcut.icon" color="white" size="18"></v-icon>
</v-avatar>
</template>
<v-list-item-title class="text-body-2">
{{ shortcut.name }}
</v-list-item-title>
<template v-slot:append>
<div class="shortcut-actions" :class="{ 'always-show': showActions }">
<v-btn
v-show="showActions"
icon
variant="text"
size="x-small"
color="primary"
@click.stop="editShortcut(index)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
v-show="showActions"
icon
variant="text"
size="x-small"
color="error"
@click.stop="deleteShortcut(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</template>
<v-divider class="my-0"></v-divider>
<v-list-item class="edit-mode-item">
<template v-slot:prepend>
<v-icon size="small" color="primary">mdi-pencil</v-icon>
</template>
<v-list-item-title class="text-caption">حالت ویرایش</v-list-item-title>
<template v-slot:append>
<v-tooltip
location="bottom"
text="نمایش دکمه‌های ویرایش و حذف"
>
<template v-slot:activator="{ props }">
<v-switch
v-model="showActions"
color="primary"
hide-details
density="compact"
class="edit-mode-switch"
v-bind="props"
></v-switch>
</template>
</v-tooltip>
</template>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<!-- دیالوگ افزودن/ویرایش دسترسی سریع -->
<v-dialog v-model="showAddDialog" max-width="500">
<v-card class="shortcut-dialog">
<v-card-title class="text-h6 pa-4 d-flex align-center">
<v-icon color="primary" class="me-2">{{ editingIndex === null ? 'mdi-plus' : 'mdi-pencil' }}</v-icon>
{{ editingIndex === null ? 'افزودن دسترسی سریع' : 'ویرایش دسترسی سریع' }}
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-6">
<v-form @submit.prevent="saveShortcut">
<v-text-field
v-model="newShortcut.name"
label="نام"
required
variant="outlined"
density="comfortable"
class="mb-4"
:rules="[v => !!v || 'نام الزامی است']"
autofocus
></v-text-field>
<v-text-field
v-model="newShortcut.path"
label="مسیر"
required
hint="برای آدرس داخلی: /acc/dashboard - برای آدرس خارجی: https://example.com"
persistent-hint
variant="outlined"
density="comfortable"
class="mb-4"
:rules="[
v => !!v || 'مسیر الزامی است',
v => isInternalPath(v) || v.startsWith('http') || 'مسیر باید با / یا http شروع شود'
]"
></v-text-field>
<v-select
v-model="newShortcut.icon"
:items="availableIcons"
label="آیکون"
required
variant="outlined"
density="comfortable"
item-title="title"
item-value="value"
return-object
:rules="[v => !!v || 'آیکون الزامی است']"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon :icon="item.raw.value" color="primary"></v-icon>
</template>
</v-list-item>
</template>
<template v-slot:selection="{ item }">
<div class="d-flex align-center">
<v-icon :icon="item.raw.value" color="primary" class="me-2"></v-icon>
<span>{{ item.raw.title }}</span>
</div>
</template>
</v-select>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="error"
variant="text"
@click="showAddDialog = false"
class="px-4"
>
انصراف
</v-btn>
<v-btn
color="primary"
@click="saveShortcut"
:loading="saving"
class="px-4"
>
{{ editingIndex === null ? 'افزودن' : 'ویرایش' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name: 'ShortcutsButton',
data() {
return {
showAddDialog: false,
editingIndex: null,
customShortcuts: [],
saving: false,
showActions: false,
newShortcut: {
name: '',
path: '',
icon: 'mdi-link'
},
availableIcons: [
{ title: 'لینک', value: 'mdi-link' },
{ title: 'داشبورد', value: 'mdi-view-dashboard' },
{ title: 'کاربران', value: 'mdi-account-group' },
{ title: 'گزارش‌ها', value: 'mdi-chart-bar' },
{ title: 'تنظیمات', value: 'mdi-cog' },
{ title: 'فایل', value: 'mdi-file' },
{ title: 'پیام', value: 'mdi-message' },
{ title: 'نوتیفیکیشن', value: 'mdi-bell' },
{ title: 'فروش', value: 'mdi-cart' },
{ title: 'خرید', value: 'mdi-cart-arrow-down' },
{ title: 'انبار', value: 'mdi-warehouse' },
{ title: 'بانک', value: 'mdi-bank' },
{ title: 'صندوق', value: 'mdi-cash-register' },
{ title: 'چک', value: 'mdi-checkbook' },
{ title: 'حسابداری', value: 'mdi-calculator' },
{ title: 'گزارش', value: 'mdi-file-document' },
{ title: 'فاکتور جدید', value: 'mdi-file-plus' },
{ title: 'فاکتور فروش', value: 'mdi-file-document-edit' },
{ title: 'فاکتور خرید', value: 'mdi-file-document-edit-outline' },
{ title: 'شخص جدید', value: 'mdi-account-plus' },
{ title: 'مشتری', value: 'mdi-account' },
{ title: 'تامین کننده', value: 'mdi-account-tie' },
{ title: 'کالا جدید', value: 'mdi-package-variant-plus' },
{ title: 'کالا', value: 'mdi-package-variant' },
{ title: 'دسته‌بندی', value: 'mdi-shape' },
{ title: 'قیمت', value: 'mdi-currency-usd' },
{ title: 'تخفیف', value: 'mdi-tag-multiple' },
{ title: 'مالیات', value: 'mdi-percent' },
{ title: 'پرداخت', value: 'mdi-cash' },
{ title: 'دریافت', value: 'mdi-cash-plus' },
{ title: 'انتقال وجه', value: 'mdi-bank-transfer' },
{ title: 'صورتحساب', value: 'mdi-receipt' },
{ title: 'سند حسابداری', value: 'mdi-book-open-variant' },
{ title: 'دفتر کل', value: 'mdi-book' },
{ title: 'دفتر روزنامه', value: 'mdi-book-open' },
{ title: 'ترازنامه', value: 'mdi-scale-balance' },
{ title: 'سود و زیان', value: 'mdi-chart-line' },
{ title: 'جریان نقدی', value: 'mdi-cash-flow' },
{ title: 'بودجه', value: 'mdi-chart-box' },
{ title: 'پیش‌بینی', value: 'mdi-chart-timeline-variant' },
{ title: 'دوره مالی', value: 'mdi-calendar-clock' },
{ title: 'سال مالی', value: 'mdi-calendar-range' },
{ title: 'بستن دوره', value: 'mdi-calendar-check' },
{ title: 'تنظیمات مالی', value: 'mdi-cog-outline' },
{ title: 'تعریف حساب', value: 'mdi-account-cog' },
{ title: 'تعریف مرکز هزینه', value: 'mdi-account-group-outline' },
{ title: 'تعریف پروژه', value: 'mdi-briefcase-outline' }
],
colors: [
'primary',
'secondary',
'success',
'info',
'warning',
'error'
]
}
},
mounted() {
this.loadShortcuts()
},
methods: {
loadShortcuts() {
const savedShortcuts = localStorage.getItem('customShortcuts')
if (savedShortcuts) {
this.customShortcuts = JSON.parse(savedShortcuts)
}
},
isInternalPath(path) {
return path.startsWith('/acc/') || path.startsWith('/')
},
navigateTo(path) {
if (!this.showActions) {
if (this.isInternalPath(path)) {
this.$router.push(path)
} else {
window.open(path, '_blank')
}
}
},
saveShortcuts() {
localStorage.setItem('customShortcuts', JSON.stringify(this.customShortcuts))
},
getRandomColor(index) {
return this.colors[index % this.colors.length]
},
async saveShortcut() {
this.saving = true
try {
if (!this.newShortcut.path) {
throw new Error('مسیر نمی‌تواند خالی باشد')
}
if (!this.isInternalPath(this.newShortcut.path) && !this.newShortcut.path.startsWith('http')) {
throw new Error('برای آدرس‌های خارجی باید از http یا https استفاده کنید')
}
const shortcutToSave = {
name: this.newShortcut.name,
path: this.newShortcut.path,
icon: this.newShortcut.icon.value
}
if (this.editingIndex === null) {
this.customShortcuts.push(shortcutToSave)
} else {
this.customShortcuts[this.editingIndex] = shortcutToSave
}
this.saveShortcuts()
this.showAddDialog = false
this.resetNewShortcut()
} catch (error) {
this.$toast.error(error.message)
} finally {
this.saving = false
}
},
editShortcut(index) {
this.editingIndex = index
this.newShortcut = { ...this.customShortcuts[index] }
this.showAddDialog = true
event.preventDefault()
event.stopPropagation()
},
deleteShortcut(index) {
this.customShortcuts.splice(index, 1)
this.saveShortcuts()
event.preventDefault()
event.stopPropagation()
},
resetNewShortcut() {
this.newShortcut = {
name: '',
path: '',
icon: 'mdi-link'
}
this.editingIndex = null
}
}
}
</script>
<style scoped>
.shortcuts-menu {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.shortcut-item {
transition: all 0.2s ease;
}
.shortcut-item:hover {
background-color: rgba(var(--v-theme-primary), 0.05);
}
.shortcut-actions {
opacity: 0;
transition: opacity 0.2s ease;
}
.shortcut-actions.always-show {
opacity: 1;
}
.shortcut-item:hover .shortcut-actions:not(.always-show) {
opacity: 1;
}
.shortcut-icon {
transition: transform 0.2s ease;
}
.shortcut-item:hover .shortcut-icon {
transform: scale(1.1);
}
.shortcut-btn {
position: relative;
}
.shortcut-btn::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background-color: rgb(var(--v-theme-primary));
transition: width 0.2s ease;
}
.shortcut-btn:hover::after {
width: 100%;
}
.edit-mode-item {
min-height: 36px !important;
padding: 0 16px !important;
}
.edit-mode-switch {
transform: scale(0.8);
margin-right: -8px;
}
.text-caption {
font-size: 0.75rem;
color: rgba(0, 0, 0, 0.6);
}
.shortcut-dialog {
border-radius: 12px;
}
.shortcut-dialog .v-card-title {
font-weight: 600;
}
.shortcut-dialog .v-card-text {
padding-top: 24px;
padding-bottom: 24px;
}
.shortcut-dialog .v-btn {
min-width: 100px;
}
</style>

View file

@ -2,10 +2,17 @@
import { defineComponent } from 'vue'
import axios from 'axios';
interface NotificationItem {
id: number;
message: string;
date: string;
url: string;
}
export default defineComponent({
name: "notifications_btn",
data: () => ({
items: [],
items: [] as NotificationItem[],
timeoutId: null as number | null, // برای ذخیره ID تایمر
}),
components: {},
@ -19,7 +26,7 @@ export default defineComponent({
}
},
methods: {
jump(item) {
jump(item: NotificationItem) {
axios.post('/api/notifications/read/' + item.id).then((response) => {
if (item.url.startsWith('http')) {
window.location.href = item.url;
@ -46,7 +53,7 @@ export default defineComponent({
<template>
<v-menu location="bottom">
<template v-slot:activator="{ props }">
<template #activator="{ props }">
<v-btn v-bind="props" stacked>
<v-badge color="error" :content="items.length">
<v-icon icon="mdi-bell"></v-icon>
@ -56,14 +63,14 @@ export default defineComponent({
<v-card prepend-icon="mdi-bell" :subtitle="$t('dialog.unread_notifications')" :title="$t('dialog.notifications')">
<v-list>
<v-list-item v-for="(item, i) in items" :key="i" :value="item" @click="jump(item)">
<template v-slot:prepend>
<template #prepend>
<v-icon color="primary" icon="mdi-alert-box-outline"></v-icon>
</template>
<v-list-item-title class="text-primary" v-text="item.message"></v-list-item-title>
<v-list-item-subtitle v-text="item.date"></v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="items.length == 0">
<template v-slot:prepend>
<template #prepend>
<v-icon color="primary" icon="mdi-cards-heart"></v-icon>
</template>
<v-list-item-title class="text-primary" v-text="$t('dialog.no_notification')"></v-list-item-title>

File diff suppressed because it is too large Load diff

View file

@ -34,10 +34,29 @@
v-for="item in filteredItems"
:key="item.id"
@click="selectItem(item)"
class="mb-1"
class="mb-2 search-result-item"
:class="{ 'selected-item': selectedItem?.id === item.id }"
>
<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>
<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-cellphone</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.nikename }}</span>
<v-chip
size="small"
:color="getBalanceColor(item.balance)"
class="balance-chip"
:class="getBalanceTextColor(item.balance)"
>
{{ formatBalance(item.balance) }}
</v-chip>
</div>
</div>
</v-list-item>
</template>
<template v-else>
@ -609,6 +628,23 @@ export default {
if (!this.loading && this.filteredItems.length === 0) {
this.showAddDialog = true;
}
},
getBalanceColor(balance) {
if (!balance) return 'grey-lighten-3';
return balance > 0 ? 'green-lighten-5' : balance < 0 ? 'red-lighten-5' : 'grey-lighten-3';
},
getBalanceTextColor(balance) {
if (!balance) return 'text-grey-darken-2';
return balance > 0 ? 'text-green-darken-2' : balance < 0 ? 'text-red-darken-2' : 'text-grey-darken-2';
},
formatBalance(balance) {
if (!balance) return 'بدون تراز';
return balance > 0 ? `بستانکار: ${this.formatNumber(balance)}` :
balance < 0 ? `بدهکار: ${this.formatNumber(Math.abs(balance))}` :
'بدون تراز';
},
formatNumber(number) {
return new Intl.NumberFormat('fa-IR').format(number);
}
},
created() {
@ -676,4 +712,74 @@ export default {
-webkit-overflow-scrolling: touch;
}
}
.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);
}
.balance-chip {
min-width: 110px;
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-chip) {
border: 1px solid rgba(0, 0, 0, 0.08);
}
:deep(.text-green-darken-2) {
color: #1b5e20 !important;
}
:deep(.text-red-darken-2) {
color: #b71c1c !important;
}
:deep(.text-grey-darken-2) {
color: #424242 !important;
}
:deep(.text-primary-dark) {
color: rgb(var(--v-theme-primary-dark)) !important;
}
:deep(.v-list) {
padding: 8px;
}
:deep(.v-card) {
border-radius: 16px;
overflow: hidden;
}
:deep(.v-card-text) {
padding: 16px;
}
</style>

View file

@ -86,6 +86,8 @@ const en_lang = {
cheque_output: "Cheque Output",
presells: "Presells",
presell_view: "View Presell",
hrm: 'HR & Payroll',
hrm_docs: 'Payroll Document',
},
};
export default en_lang

View file

@ -190,6 +190,8 @@ const fa_lang = {
plugins_invoices: "صورت حساب‌ها",
repservice: "مدیریت تعمیرگاه",
repservice_reqs: "درخواست‌ها",
hrm: 'منابع انسانی',
hrm_docs: 'سند حقوق',
},
time: {
month: "{id} ماه",
@ -494,6 +496,32 @@ const fa_lang = {
presell_info: "اطلاعات پیش فاکتور",
financial_info: "اطلاعات مالی",
invoice_items: "اقلام فاکتور",
hrm: {
title: "سند حقوق",
date: "تاریخ",
description: "توضیحات",
person: "شخص",
base_salary: "حقوق پایه",
overtime: "اضافه کار",
shift: "حق شیفت",
night: "شب کاری",
total: "جمع کل",
row_description: "شرح",
add_new_row: "افزودن سطر جدید",
no_data: "هیچ داده‌ای ثبت نشده است",
delete_confirm: "آیا مطمئن هستید که می‌خواهید این سند را حذف کنید؟",
save_success: "سند با موفقیت ثبت شد",
edit_success: "سند با موفقیت ویرایش شد",
delete_success: "سند با موفقیت حذف شد",
load_error: "خطا در دریافت اطلاعات",
save_error: "خطا در ذخیره اطلاعات",
delete_error: "خطا در حذف سند",
required_fields: {
date: "تاریخ الزامی است",
description: "توضیحات الزامی است",
person: "انتخاب شخص الزامی است"
}
}
},
app: {
loading: "در حال بارگذاری...",

View file

@ -764,6 +764,12 @@ const router = createRouter({
component: () =>
import('../views/acc/plugins/resamap/intro.vue'),
},
{
path: 'plugins/hrm/intro',
name: 'plugin_hrm_intro',
component: () =>
import('../views/acc/plugins/hrm/intro.vue'),
},
{
path: 'plugins/noghre/intro',
name: 'plugin_noghre_intro',
@ -926,6 +932,24 @@ const router = createRouter({
component: () =>
import('../views/acc/shareholder/list.vue'),
},
{
path: 'hrm/docs/list',
name: 'hrm_docs_list',
component: () =>
import('../views/acc/plugins/hrm/docs/list.vue'),
},
{
path: 'hrm/docs/mod/:id?',
name: 'hrm_docs_mod',
component: () =>
import('../views/acc/plugins/hrm/docs/mod.vue'),
},
{
path: 'hrm/docs/view/:id?',
name: 'hrm_docs_view',
component: () =>
import('../views/acc/plugins/hrm/docs/view.vue'),
},
],
},
{

14
webUI/src/types/vue3-qrcode-reader.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
declare module 'vue3-qrcode-reader' {
import { DefineComponent } from 'vue'
export const QrcodeStream: DefineComponent<{
camera?: string
torch?: boolean
track?: (location: any, ctx: CanvasRenderingContext2D) => void
}>
export const QrcodeCapture: DefineComponent<{
camera?: string
torch?: boolean
}>
}

View file

@ -7,7 +7,7 @@
<div class="fof">
<h1>
<v-empty-state :headline="$t('static.not_found')" title="404" :text="$t('static.not_found_info')"
image="/img/logo-blue.png"></v-empty-state>
image="/u/img/logo-blue.png"></v-empty-state>
<v-btn color="success" to="/" prepend-icon="mdi-home">{{ $t('static.home_page') }}</v-btn>
</h1>
</div>

View file

@ -11,6 +11,7 @@ import Currency_cob from '@/components/application/combobox/currency_cob.vue';
import clock from '@/components/application/clock.vue';
import CalculatorButton from '@/components/application/buttons/CalculatorButton.vue'
import SecretDialog from '@/components/application/buttons/SecretDialog.vue';
import ShortcutsButton from '@/components/application/buttons/ShortcutsButton.vue';
export default {
data() {
return {
@ -175,7 +176,8 @@ export default {
{ path: '/acc/archive/order/list', key: '.', label: this.$t('drawer.archive_log'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/plugin-center/list', key: '/', label: this.$t('drawer.plugins_list'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/plugin-center/my', key: '\\', label: this.$t('drawer.my_plugins'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner }
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs },
];
},
restorePermissions(shortcuts) {
@ -283,14 +285,15 @@ export default {
Currency_cob,
clock,
CalculatorButton,
SecretDialog
SecretDialog,
ShortcutsButton
}
};
</script>
<template>
<v-system-bar color="primaryLight2">
<v-avatar image="/img/logo-blue.png" size="20" class="me-2 d-none d-sm-flex" />
<v-avatar image="/u/img/logo-blue.png" size="20" class="me-2 d-none d-sm-flex" />
<span class="d-none d-sm-flex">{{ siteSlogan }}</span>
<v-avatar :image="apiUrl + '/front/avatar/file/get/' + business.id" size="20" class="me-2 d-flex d-sm-none" />
<span class="d-flex d-sm-none">{{ business.name }}</span>
@ -784,6 +787,26 @@ export default {
</template>
</v-list-item>
</v-list-group>
<v-list-group v-show="isPluginActive('hrm') && permissions.plugHrmDocs">
<template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.hrm')">
<template v-slot:prepend><v-icon icon="mdi-account-cash" color="primary"></v-icon></template>
</v-list-item>
</template>
<v-list-item to="/acc/hrm/docs/list">
<v-list-item-title>
{{ $t('drawer.hrm_docs') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/hrm/docs/list') }}</span>
</v-list-item-title>
<template v-slot:append v-if="permissions.plugHrmDocs">
<v-tooltip :text="$t('dialog.add_new')" location="end">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" to="/acc/hrm/docs/mod/" />
</template>
</v-tooltip>
</template>
</v-list-item>
</v-list-group>
<v-list-item class="text-dark" v-if="permissions.owner" to="/acc/sms/panel">
<template v-slot:prepend><v-icon icon="mdi-message-cog" color="primary"></v-icon></template>
<v-list-item-title>
@ -884,10 +907,11 @@ export default {
<v-tooltip text="جادوگر" location="bottom">
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-robot</v-icon>
</v-btn>
<v-icon>mdi-robot</v-icon>
</v-btn>
</template>
</v-tooltip>
<ShortcutsButton />
<CalculatorButton />
<SecretDialog />
<v-dialog v-model="showShortcutsDialog" max-width="800" scrollable>

View file

@ -120,8 +120,38 @@
<v-divider class="my-2"></v-divider>
<!-- فیلتر بازه زمانی -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
</v-list-item-title>
<v-row>
<v-col cols="12">
<v-checkbox
v-model="timeFilters.find(f => f.value === 'custom').checked"
label="بازه زمانی"
@change="handleCustomDateFilterChange"
hide-details
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.from"
label="از تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.to"
label="تا تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
</v-row>
</v-list-item>
<!-- فیلترهای زمانی -->
<v-list-item v-for="(filter, index) in timeFilters" :key="index" class="text-dark">
<v-list-item v-for="(filter, index) in timeFilters.filter(f => f.value !== 'custom')" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
hide-details />
@ -242,6 +272,7 @@ import { debounce } from 'lodash';
import { getApiUrl } from '/src/hesabixConfig';
import moment from 'jalali-moment';
import HesabdariTreeView from '@/components/forms/HesabdariTreeView.vue';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
const apiUrl = getApiUrl();
axios.defaults.baseURL = apiUrl;
@ -255,12 +286,17 @@ const searchQuery = ref('');
const timeFilter = ref('all');
const expanded = ref([]);
const selectedAccountId = ref('67');
const dateRange = ref({
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
});
// فیلترهای زمانی
const timeFilters = ref([
{ label: 'امروز', value: 'today', checked: false },
{ label: 'این هفته', value: 'week', checked: false },
{ label: 'این ماه', value: 'month', checked: false },
{ label: 'بازه زمانی', value: 'custom', checked: false },
{ label: 'همه', value: 'all', checked: true },
]);
@ -317,6 +353,93 @@ const resetAccountFilter = () => {
fetchData();
};
// دیبونس برای جستجو
const debouncedSearch = debounce(() => fetchData(), 500);
// دیبونس برای تغییر تاریخ
const debouncedFetchData = debounce(() => {
fetchData();
}, 500);
// اصلاح متد handleDateRangeChange
const handleDateRangeChange = () => {
if (dateRange.value.from && dateRange.value.to) {
// تبدیل تاریخهای شمسی به آبجکت moment
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa');
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa');
// بررسی اعتبار بازه زمانی
if (fromDate.isAfter(toDate)) {
Swal.fire({
text: 'تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد',
icon: 'error',
confirmButtonText: 'قبول'
});
// پاک کردن تاریخها
dateRange.value = {
from: null,
to: null
};
return;
}
// ارسال درخواست با تاخیر
debouncedFetchData();
}
};
// اصلاح متد handleCustomDateFilterChange
const handleCustomDateFilterChange = (checked) => {
if (checked) {
// غیرفعال کردن سایر فیلترها
timeFilters.value.forEach(filter => {
if (filter.value !== 'custom') {
filter.checked = false;
}
});
timeFilter.value = 'custom';
// تنظیم تاریخهای پیشفرض
dateRange.value = {
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
};
// ارسال درخواست با تاریخهای پیشفرض
debouncedFetchData();
} else {
// اگر چکباکس بازه زمانی غیرفعال شد، فیلتر "همه" را فعال کن
timeFilters.value.forEach(filter => {
filter.checked = filter.value === 'all';
});
timeFilter.value = 'all';
// پاک کردن تاریخها
dateRange.value = {
from: null,
to: null
};
debouncedFetchData();
}
};
// اصلاح متد applyTimeFilter
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value;
});
timeFilter.value = value;
// اگر فیلتر بازه زمانی غیرفعال شد، تاریخها را پاک کن
if (value !== 'custom') {
dateRange.value = {
from: null,
to: null
};
}
fetchData();
};
// فچ کردن دادهها از سرور
const fetchData = async () => {
try {
@ -329,20 +452,37 @@ const fetchData = async () => {
if (timeFilter.value) {
filters.timeFilter = timeFilter.value;
const today = moment().locale('fa').format('YYYY/MM/DD');
switch (timeFilter.value) {
case 'today':
filters.dateFrom = today;
filters.dateTo = today;
break;
case 'week':
filters.dateFrom = moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD');
filters.dateTo = today;
break;
case 'month':
filters.dateFrom = moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD');
filters.dateTo = today;
break;
if (timeFilter.value === 'custom' && dateRange.value.from && dateRange.value.to) {
// تبدیل تاریخهای شمسی به فرمت مورد نیاز
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD');
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD');
filters.date = {
from: fromDate,
to: toDate
};
} else {
const today = moment().locale('fa').format('YYYY/MM/DD');
switch (timeFilter.value) {
case 'today':
filters.date = {
from: today,
to: today
};
break;
case 'week':
filters.date = {
from: moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD'),
to: today
};
break;
case 'month':
filters.date = {
from: moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD'),
to: today
};
break;
}
}
}
@ -368,6 +508,8 @@ const fetchData = async () => {
},
};
console.log('Request payload:', payload); // برای دیباگ
const response = await axios.post('/api/cost/list/search', {
type: 'cost',
...payload,
@ -396,18 +538,6 @@ const fetchData = async () => {
}
};
// دیبونس برای جستجو
const debouncedSearch = debounce(() => fetchData(), 500);
// اعمال فیلتر زمانی
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value;
});
timeFilter.value = value;
fetchData();
};
// حذف یک آیتم
const deleteItem = async (code) => {
const result = await Swal.fire({
@ -589,6 +719,13 @@ const deleteGroup = async () => {
}
};
// اضافه کردن watch برای تغییرات تاریخها
watch([() => dateRange.value.from, () => dateRange.value.to], () => {
if (timeFilter.value === 'custom') {
handleDateRangeChange();
}
}, { deep: true });
// Watchers
watch(() => serverOptions.value.page, () => {
selectedItems.value.clear();

View file

@ -7,7 +7,9 @@ export default defineComponent({
siteName:''
}},
created(){
this.siteName = getSiteName();
getSiteName().then((name) => {
this.siteName = name;
});
}
})
</script>
@ -15,7 +17,7 @@ export default defineComponent({
<template>
<main id="main-container p-0 m-0">
<!-- Hero -->
<div class="bg-image" style="background-image: url('/img/plugins/accpro/intro.png');">
<div class="bg-image" style="background-image: url('/u/img/plugins/accpro/intro.png');">
<div class="bg-black-75">
<div class="content content-top content-full text-center">
<h1 class="text-white"><i class="fa fa-shop"></i></h1>

View file

@ -0,0 +1,70 @@
<template>
<v-container>
<v-card>
<v-card-title class="d-flex align-center">
{{ $t('drawer.hrm_docs') }}
<v-spacer></v-spacer>
<v-btn color="primary" prepend-icon="mdi-plus" to="/acc/hrm/docs/mod/">
{{ $t('dialog.add_new') }}
</v-btn>
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-eye"
variant="text"
size="small"
:to="'/acc/hrm/docs/view/' + item.id"
></v-btn>
<v-btn
icon="mdi-pencil"
variant="text"
size="small"
:to="'/acc/hrm/docs/mod/' + item.id"
></v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
export default {
data() {
return {
loading: false,
headers: [
{ title: this.$t('field.id'), key: 'id' },
{ title: this.$t('field.date'), key: 'date' },
{ title: this.$t('field.employee'), key: 'employee' },
{ title: this.$t('field.amount'), key: 'amount' },
{ title: this.$t('field.status'), key: 'status' },
{ title: this.$t('field.actions'), key: 'actions', sortable: false }
],
items: []
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
this.loading = true
try {
const response = await this.$axios.post('/api/hrm/docs/list')
this.items = response.data
} catch (error) {
console.error('Error loading data:', error)
}
this.loading = false
}
}
}
</script>

View file

@ -0,0 +1,393 @@
<template>
<v-toolbar color="toolbar" flat class="rounded-t mb-4">
<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()" variant="text" icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-toolbar-title class="text-h6">{{ isEdit ? $t('dialog.edit') : $t('dialog.add_new') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip :text="$t('dialog.save')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-content-save" color="success" @click="validateAndSave"></v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="isEdit" :text="$t('dialog.delete')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-delete" color="error" @click="deleteDialog = true"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-container>
<v-form ref="form" v-model="valid">
<v-row>
<v-col cols="12" sm="6" md="6">
<Hdatepicker v-model="form.date" :label="$t('dialog.hrm.date')"
:rules="[v => !!v || $t('dialog.hrm.required_fields.date')]" required />
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field v-model="form.description" :label="$t('dialog.hrm.description')"
:rules="[v => !!v || $t('dialog.hrm.required_fields.description')]" required></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-table class="border rounded d-none d-sm-table" style="width: 100%;">
<thead>
<tr style="background-color: #0D47A1; color: white;">
<th class="text-center pa-1" style="width: 100px;">شخص</th>
<th class="text-center pa-1">حقوق پایه</th>
<th class="text-center pa-1">اضافه کار</th>
<th class="text-center pa-1">حق شیفت</th>
<th class="text-center pa-1">شب کاری</th>
<th class="text-center pa-1" style="width: 150px;">جمع کل</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in tableItems" :key="index">
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '48px' }">
<td class="text-center pa-1" style="width: 170px;">
<Hpersonsearch v-model="row.person" label="شخص" density="compact" hide-details class="my-0" style="font-size: 0.8rem;"></Hpersonsearch>
</td>
<td class="text-center pa-1" style="width: 120px;">
<Hnumberinput v-model="row.baseSalary" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
</td>
<td class="text-center pa-1" style="width: 120px;">
<Hnumberinput v-model="row.overtime" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
</td>
<td class="text-center pa-1" style="width: 120px;">
<Hnumberinput v-model="row.shift" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
</td>
<td class="text-center pa-1" style="width: 120px;">
<Hnumberinput v-model="row.night" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
</td>
<td class="text-center font-weight-bold pa-1" style="width: 120px;">
{{ calculateTotal(row).toLocaleString('fa-IR') }}
</td>
</tr>
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '48px' }">
<td colspan="4" class="pa-1">
<v-text-field v-model="row.description" density="compact" hide-details
:placeholder="$t('dialog.hrm.row_description')" class="my-0" style="font-size: 0.8rem;"></v-text-field>
</td>
<td class="text-center pa-1" style="width: 120px;">
<v-tooltip text="حذف" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-delete" variant="text" size="small" color="error" @click="removeRow(index)"></v-btn>
</template>
</v-tooltip>
</td>
</tr>
</template>
<tr v-if="tableItems.length === 0">
<td colspan="6" class="text-center text-grey pa-1">{{ $t('dialog.hrm.no_data') }}</td>
</tr>
<tr>
<td colspan="6" class="text-center pa-1" style="height: 48px;">
<v-btn color="primary" prepend-icon="mdi-plus" size="small" @click="addRow">{{ $t('dialog.hrm.add_new_row') }}</v-btn>
</td>
</tr>
<tr v-if="tableItems.length > 0" style="background-color: #E3F2FD;">
<td class="text-center pa-1 font-weight-bold">جمع کل</td>
<td class="text-center pa-1 font-weight-bold">{{ calculateColumnTotal('baseSalary').toLocaleString('fa-IR') }}</td>
<td class="text-center pa-1 font-weight-bold">{{ calculateColumnTotal('overtime').toLocaleString('fa-IR') }}</td>
<td class="text-center pa-1 font-weight-bold">{{ calculateColumnTotal('shift').toLocaleString('fa-IR') }}</td>
<td class="text-center pa-1 font-weight-bold">{{ calculateColumnTotal('night').toLocaleString('fa-IR') }}</td>
<td class="text-center pa-1 font-weight-bold">{{ calculateColumnTotal('total').toLocaleString('fa-IR') }}</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-form>
<v-snackbar v-model="showSuccess" color="success" timeout="3000">
{{ successMessage }}
</v-snackbar>
<v-snackbar v-model="showError" color="error" timeout="3000">
{{ errorMessage }}
</v-snackbar>
<v-dialog v-model="deleteDialog" max-width="400">
<v-card>
<v-card-title class="text-h5">{{ $t('dialog.hrm.title') }}</v-card-title>
<v-card-text>{{ $t('dialog.hrm.delete_confirm') }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="deleteDialog = false">{{ $t('dialog.cancel') }}</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete" :loading="loading">{{ $t('dialog.delete') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue';
import Hnumberinput from '@/components/forms/Hnumberinput.vue';
export default {
components: { Hdatepicker, Hpersonsearch, Hnumberinput },
data() {
return {
valid: false,
isEdit: false,
loading: false,
deleteDialog: false,
showSuccess: false,
showError: false,
successMessage: '',
errorMessage: '',
form: {
date: '',
description: ''
},
tableItems: [],
}
},
mounted() {
const id = this.$route.params.id
if (id) {
this.isEdit = true
this.loadData(id)
}
},
methods: {
async loadData(id) {
try {
const response = await this.$axios.post('/api/hrm/docs/get/' + id)
this.form = response.data
} catch (error) {
this.errorMessage = 'خطا در دریافت اطلاعات';
this.showError = true;
}
},
validateAndSave() {
if (!this.$refs.form.validate()) {
this.errorMessage = this.$t('validator.form_invalid');
this.showError = true;
return;
}
// بررسی وجود حداقل یک سطر در جدول
if (this.tableItems.length === 0) {
this.errorMessage = this.$t('dialog.hrm.no_data');
this.showError = true;
return;
}
// بررسی انتخاب شخص در تمام سطرها
const hasInvalidPerson = this.tableItems.some(row => !row.person);
if (hasInvalidPerson) {
this.errorMessage = this.$t('dialog.hrm.required_fields.person');
this.showError = true;
return;
}
this.save();
},
async save() {
try {
this.loading = true;
const url = this.isEdit ? '/api/hrm/docs/update' : '/api/hrm/docs/insert'
await this.$axios.post(url, this.form)
this.successMessage = this.isEdit ? this.$t('dialog.hrm.edit_success') : this.$t('dialog.hrm.save_success');
this.showSuccess = true;
setTimeout(() => {
this.$router.push('/acc/hrm/docs/list')
}, 1200)
} catch (error) {
this.errorMessage = this.$t('dialog.hrm.save_error');
this.showError = true;
} finally {
this.loading = false;
}
},
async confirmDelete() {
try {
this.loading = true;
await this.$axios.post('/api/hrm/docs/delete', { id: this.$route.params.id })
this.successMessage = 'سند با موفقیت حذف شد';
this.showSuccess = true;
setTimeout(() => {
this.$router.push('/acc/hrm/docs/list')
}, 1200)
} catch (error) {
this.errorMessage = 'خطا در حذف سند';
this.showError = true;
} finally {
this.loading = false;
this.deleteDialog = false;
}
},
addRow() {
this.tableItems.push({
person: null,
description: '',
baseSalary: 0,
overtime: 0,
shift: 0,
night: 0
});
},
removeRow(index) {
this.tableItems.splice(index, 1);
},
calculateTotal(row) {
const base = Number(row.baseSalary) || 0;
const overtime = Number(row.overtime) || 0;
const shift = Number(row.shift) || 0;
const night = Number(row.night) || 0;
return base + overtime + shift + night;
},
recalculateTotals() {
// Implementation of recalculateTotals method
},
calculateColumnTotal(column) {
return this.tableItems.reduce((sum, row) => {
if (column === 'total') {
return sum + this.calculateTotal(row);
}
return sum + (Number(row[column]) || 0);
}, 0);
},
}
}
</script>
<style scoped>
.bank-card {
border-right: 4px solid #1976D2 !important;
}
.cashdesk-card {
border-right: 4px solid #4CAF50 !important;
}
.salary-card {
border-right: 4px solid #FF9800 !important;
}
.tabs-container {
background-color: #f5f5f5;
}
.payment-card :deep(.v-card-item) {
min-height: 40px;
}
.payment-card :deep(.v-card-title) {
font-size: 0.875rem;
line-height: 1.25rem;
}
.payment-card :deep(.v-card-text) {
padding-top: 8px;
padding-bottom: 8px;
}
:deep(.v-overlay__content) {
z-index: 9999 !important;
}
:deep(.v-menu__content) {
z-index: 9999 !important;
}
:deep(.v-dialog) {
z-index: 9999 !important;
}
:deep(.v-date-picker) {
z-index: 9999 !important;
}
.settings-section-card {
height: 100%;
transition: all 0.3s ease;
}
.settings-section-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.settings-section-title {
background-color: #f5f5f5;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.settings-section-content {
padding: 20px;
}
.settings-section-card :deep(.v-switch) {
margin-bottom: 8px;
}
.settings-section-card :deep(.v-switch .v-label) {
font-size: 0.9rem;
color: #424242;
}
.settings-section-card :deep(.v-switch.v-input--disabled) {
opacity: 0.6;
}
.settings-section-card :deep(.v-select) {
margin-top: 8px;
}
.settings-section-card :deep(.v-divider) {
margin: 16px 0;
}
.draft-dialog {
border-radius: 12px;
overflow: hidden;
}
.draft-dialog-title {
background-color: #f5f5f5;
padding: 16px;
font-size: 1.25rem;
font-weight: 500;
display: flex;
align-items: center;
}
.draft-dialog-content {
padding: 24px;
}
.draft-message {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 16px 0;
}
.draft-message p {
margin: 8px 0;
}
.draft-dialog-actions {
padding: 16px;
background-color: #f5f5f5;
}
:deep(.v-btn) {
text-transform: none;
letter-spacing: 0;
font-weight: 500;
}
:deep(.v-btn--elevated) {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
:deep(.v-btn--outlined) {
border-width: 1px;
}
</style>

View file

@ -0,0 +1,89 @@
<template>
<v-container>
<v-card>
<v-card-title class="d-flex align-center">
{{ $t('drawer.hrm_docs') }}
<v-spacer></v-spacer>
<v-btn
color="primary"
prepend-icon="mdi-pencil"
:to="'/acc/hrm/docs/mod/' + $route.params.id"
>
{{ $t('dialog.edit') }}
</v-btn>
</v-card-title>
<v-card-text>
<v-row v-if="!loading">
<v-col cols="12" md="6">
<v-list>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-account"></v-icon>
</template>
<v-list-item-title>{{ $t('field.employee') }}</v-list-item-title>
<v-list-item-subtitle>{{ item.employee }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-currency-usd"></v-icon>
</template>
<v-list-item-title>{{ $t('field.amount') }}</v-list-item-title>
<v-list-item-subtitle>{{ item.amount }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-calendar"></v-icon>
</template>
<v-list-item-title>{{ $t('field.date') }}</v-list-item-title>
<v-list-item-subtitle>{{ item.date }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-check-circle"></v-icon>
</template>
<v-list-item-title>{{ $t('field.status') }}</v-list-item-title>
<v-list-item-subtitle>{{ item.status }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-col>
</v-row>
<v-progress-circular
v-else
indeterminate
color="primary"
></v-progress-circular>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" @click="$router.back()">
{{ $t('dialog.back') }}
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script>
export default {
data() {
return {
loading: true,
item: {}
}
},
mounted() {
this.loadData()
},
methods: {
async loadData() {
try {
const response = await this.$axios.post('/api/hrm/docs/get/' + this.$route.params.id)
this.item = response.data
} catch (error) {
console.error('Error loading data:', error)
}
this.loading = false
}
}
}
</script>

View file

@ -0,0 +1,127 @@
<script lang="ts">
import {defineComponent} from 'vue'
import {getApiUrl, getSiteName} from '@/hesabixConfig'
export default defineComponent({
name: "intro",
data:()=>{return{
siteName:''
}},
async created(){
this.siteName = await getSiteName();
}
})
</script>
<template>
<main id="main-container pt-0 mt-o">
<!-- Hero -->
<div class="bg-image" style="background-image: url('/u/img/plugins/hrm/hrm.jpg');">
<div class="bg-black-75">
<div class="content content-top content-full text-center">
<h1 class="text-white"><i class="fa fa-users"></i></h1>
<h1 class="fw-bold text-white mt-5 mb-3"> افزونه مدیریت منابع انسانی </h1>
<h2 class="h3 fw-normal text-white-75 mb-5">مدیریت هوشمند پرسنل، حقوق و دستمزد و حضور و غیاب با یک افزونه قدرتمند</h2>
<RouterLink to="/acc/plugin-center/view-end/hrm">
<span class="badge rounded-pill bg-primary fs-base px-3 py-2 me-2 m-1">
<i class="fa fa-shopping-cart me-1"></i> خرید نسخه آزمایشی </span>
</RouterLink>
</div>
</div>
</div>
<!-- END Hero -->
<!-- Page Content -->
<div class="content content-full">
<div class="row justify-content-center">
<div class="col-sm-11 py-2">
<!-- Story -->
<article class="story justify-content-between">
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i>
این افزونه در حال توسعه و آزمایشی است. در نسخه اولیه، تنها از سند حقوق کارکنان پشتیبانی میکند. خرید شما باعث سرعت بیشتر در توسعه و بهبود قابلیتهای آن میشود.
</div>
<p class="justify-content-between">
مدیریت منابع انسانی یکی از مهمترین بخشهای هر سازمان است که نیاز به دقت و نظم بالایی دارد. افزونه مدیریت منابع انسانی حسابیکس با ارائه قابلیتهای متنوع، مدیریت پرسنل، محاسبه حقوق و دستمزد و کنترل حضور و غیاب را به سادهترین شکل ممکن فراهم میکند.
</p>
<p>
<strong>قابلیتهای نسخه اولیه:</strong>
</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">
<i class="fa fa-check text-success me-2"></i>
ثبت سند حقوق کارکنان با در نظر گرفتن موارد قانونی
</li>
</ul>
<p>
<strong>قابلیتهای در دست توسعه:</strong>
</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
سیستم حضور و غیاب هوشمند
</li>
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
مدیریت مرخصیها و غیبتها
</li>
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
ثبت مدارک و سوابق کاری
</li>
</ul>
<p>
این افزونه به صورت کامل با سیستم حسابداری حسابیکس یکپارچه شده و تمامی عملیات مالی مربوط به حقوق و دستمزد را به صورت خودکار در سیستم حسابداری ثبت میکند. همچنین امکان صدور فیش حقوقی و گزارشهای مالی متنوع را فراهم میکند.
</p>
<p>
با استفاده از این افزونه، دیگر نیازی به نرم افزارهای جداگانه برای مدیریت منابع انسانی نخواهید داشت. تمامی اطلاعات در یک سیستم یکپارچه ذخیره میشود و دسترسی به آنها از هر مکان و در هر زمان امکانپذیر است.
</p>
</article>
<!-- END Story -->
</div>
</div>
</div>
<!-- END Page Content -->
</main>
</template>
<style scoped>
.bg-image {
background-size: cover;
background-position: center;
min-height: 400px;
position: relative;
}
.bg-black-75 {
background-color: rgba(0, 0, 0, 0.75);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
}
.story {
line-height: 1.8;
font-size: 1.1rem;
}
.alert {
border-radius: 8px;
margin-bottom: 2rem;
}
.list-group-item {
border: none;
padding: 0.75rem 1.25rem;
background-color: transparent;
}
.list-group-item i {
width: 20px;
text-align: center;
}
</style>

View file

@ -9,7 +9,9 @@ export default defineComponent({
}
},
created() {
this.siteName = getSiteName();
getSiteName().then(name => {
this.siteName = name;
});
}
})
</script>
@ -17,7 +19,7 @@ export default defineComponent({
<template>
<main id="main-container p-0 m-0">
<!-- Hero -->
<div class="bg-image" style="background-image: url('/img/plugins/repservice.png');">
<div class="bg-image" style="background-image: url('/u/img/plugins/repservice.png');">
<div class="bg-black-75">
<div class="content content-top content-full text-center">
<h1 class="text-white"><i class="fa fa-shop"></i></h1>
@ -30,8 +32,6 @@ export default defineComponent({
<span class="badge rounded-pill bg-primary fs-base px-3 py-2 me-2 m-1">
<i class="fa fa-user-circle me-1"></i> خرید </span>
</RouterLink>
<br>
<i class="fa fa-arrow-down text-white"></i>
</div>
</div>
</div>

View file

@ -216,15 +216,24 @@
</template>
</v-data-table-server>
<div class="footer-summary">
<div class="summary-item">
<span class="summary-label">جمع کل فاکتورهای صفحه:</span>
<span class="summary-value">{{ $filters.formatNumber(sumTotal) }}</span>
<div class="summary-items">
<div class="summary-item">
<span class="summary-label">جمع کل فاکتورهای صفحه:</span>
<span class="summary-value">{{ $filters.formatNumber(sumTotal) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">جمع موارد انتخاب شده:</span>
<span class="summary-value">{{ $filters.formatNumber(sumSelected) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">جمع سود موارد انتخاب شده:</span>
<span class="summary-value" :class="{'text-success': sumSelectedProfit >= 0, 'text-danger': sumSelectedProfit < 0}">
{{ $filters.formatNumber(Math.abs(sumSelectedProfit)) }}
<span v-if="sumSelectedProfit < 0">(زیان)</span>
</span>
</div>
</div>
</div>
<div class="summary-item">
<span class="summary-label">جمع موارد انتخاب شده:</span>
<span class="summary-value">{{ $filters.formatNumber(sumSelected) }}</span>
</div>
</div>
<v-dialog v-model="showColumnDialog" max-width="500px">
<v-card>
<v-toolbar dark>
@ -344,6 +353,7 @@ export default defineComponent({
},
plugins: {},
sumSelected: 0,
sumSelectedProfit: 0,
sumTotal: 0,
itemsSelected: [],
searchValue: '',
@ -693,6 +703,7 @@ export default defineComponent({
itemsSelected: {
handler(val) {
this.sumSelected = 0;
this.sumSelectedProfit = 0;
this.itemsSelected.forEach((code) => {
const selectedItem = this.items.find(item => item.code === code);
if (selectedItem) {
@ -702,6 +713,7 @@ export default defineComponent({
} else {
this.sumSelected += amount;
}
this.sumSelectedProfit += selectedItem.profit || 0;
}
});
},
@ -715,21 +727,29 @@ export default defineComponent({
.footer-summary {
background-color: #f5f5f5;
padding: 12px 24px;
display: flex;
justify-content: space-between;
border-top: 1px solid #e0e0e0;
margin-top: 8px;
}
.summary-items {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: space-between;
}
.summary-item {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 200px;
}
.summary-label {
font-weight: 500;
color: #666;
white-space: nowrap;
}
.summary-value {
@ -737,6 +757,18 @@ export default defineComponent({
color: #1976d2;
}
@media (max-width: 600px) {
.summary-items {
flex-direction: column;
gap: 8px;
}
.summary-item {
min-width: 100%;
justify-content: space-between;
}
}
.data-table-wrapper {
margin-bottom: 0;
}

View file

@ -46,35 +46,46 @@
<mostdes v-model="invoiceDescription" :submitData="{ id: null, des: invoiceDescription }" type="sell" label=""></mostdes>
</template>
</v-text-field>
<v-table class="border rounded d-none d-sm-table" style="width: 100%;">
<v-table class="border rounded d-none d-sm-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #0D47A1; color: white;">
<th class="text-center px-2" style="width: 50px;">ردیف</th>
<th class="text-center">نام کالا</th>
<th class="text-center">تعداد</th>
<th class="text-center">قیمت</th>
<th class="text-center">تخفیف</th>
<th class="text-center" style="width: 150px;">جمع کل</th>
<th class="text-center">جمع کل</th>
</tr>
</thead>
<tbody>
<template v-for="(item, index) in items" :key="index">
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '64px' }">
<td class="text-center" style="min-width: 200px;">
<td class="text-center px-2">
<span class="text-subtitle-2">{{ index + 1 }}</span>
</td>
<td class="text-center px-2">
<Hcommoditysearch v-model="item.name" density="compact" hide-details class="my-0" style="font-size: 0.8rem;" return-object @update:modelValue="handleCommodityChange(item)"></Hcommoditysearch>
</td>
<td class="text-center" style="width: 100px;">
<td class="text-center px-2">
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
</td>
<td class="text-center" style="width: 120px;">
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
<td class="text-center px-2">
<div class="d-flex align-center justify-center">
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
<v-tooltip v-if="item.name && item.price < item.name.priceBuy" text="قیمت فروش کمتر از قیمت خرید است" location="bottom">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="warning" size="small" class="mr-1">mdi-alert</v-icon>
</template>
</v-tooltip>
</div>
</td>
<td class="text-center" style="width: 150px;">
<td class="text-center px-2">
<div class="d-flex align-center">
<Hnumberinput v-if="item.showPercentDiscount" v-model="item.discountPercent" density="compact" suffix="%" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;">
<template v-slot:prepend>
<v-tooltip text="تخفیف درصدی" location="bottom">
<template v-slot:activator="{ props }">
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" style="margin: 0; padding: 0;" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
</template>
</v-tooltip>
</template>
@ -83,14 +94,14 @@
<template v-slot:prepend>
<v-tooltip text="تخفیف مبلغی" location="bottom">
<template v-slot:activator="{ props }">
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" style="margin: 0; padding: 0;" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
</template>
</v-tooltip>
</template>
</Hnumberinput>
</div>
</td>
<td class="text-center font-weight-bold" style="width: 120px;">
<td class="text-center font-weight-bold px-2">
{{ item.total.toLocaleString('fa-IR') }}
</td>
</tr>
@ -127,35 +138,42 @@
<v-card-text>
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-2 font-weight-bold">ردیف:</span>
<span>{{ index + 1 }}</span>
<span class="text-subtitle-2">{{ index + 1 }}</span>
</div>
<div class="mb-2">
<Hcommoditysearch v-model="item.name" density="compact" label="نام کالا" hide-details class="my-0" style="font-size: 0.8rem;" return-object @update:modelValue="handleCommodityChange(item)"></Hcommoditysearch>
<Hcommoditysearch v-model="item.name" density="compact" label="نام کالا" hide-details class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;" return-object @update:modelValue="handleCommodityChange(item)"></Hcommoditysearch>
</div>
<div class="d-flex justify-space-between mb-2">
<div style="width: 48%;">
<Hnumberinput v-model="item.count" density="compact" label="تعداد" hide-details class="my-0" style="font-size: 0.8rem;" @update:modelValue="recalculateTotals"></Hnumberinput>
<div class="flex-grow-1 mr-2">
<Hnumberinput v-model="item.count" density="compact" label="تعداد" hide-details class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;" @update:modelValue="recalculateTotals"></Hnumberinput>
</div>
<div style="width: 48%;">
<Hnumberinput v-model="item.price" density="compact" label="قیمت" hide-details class="my-0" style="font-size: 0.8rem;" @update:modelValue="recalculateTotals"></Hnumberinput>
<div class="flex-grow-1">
<div class="d-flex align-center">
<Hnumberinput v-model="item.price" density="compact" label="قیمت" hide-details class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;" @update:modelValue="recalculateTotals"></Hnumberinput>
<v-tooltip v-if="item.name && item.price < item.name.priceBuy" text="قیمت فروش کمتر از قیمت خرید است" location="bottom">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="warning" size="small" class="mr-1" style="margin: 0; padding: 0;">mdi-alert</v-icon>
</template>
</v-tooltip>
</div>
</div>
</div>
<div class="mb-2">
<div class="d-flex align-center">
<Hnumberinput v-if="item.showPercentDiscount" v-model="item.discountPercent" density="compact" label="تخفیف" suffix="%" hide-details @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;">
<Hnumberinput v-if="item.showPercentDiscount" v-model="item.discountPercent" density="compact" label="تخفیف" suffix="%" hide-details @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;">
<template v-slot:prepend>
<v-tooltip text="تخفیف درصدی" location="bottom">
<template v-slot:activator="{ props }">
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" style="margin: 0; padding: 0;" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
</template>
</v-tooltip>
</template>
</Hnumberinput>
<Hnumberinput v-else v-model="item.discountAmount" density="compact" label="تخفیف" hide-details @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;">
<Hnumberinput v-else v-model="item.discountAmount" density="compact" label="تخفیف" hide-details @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;">
<template v-slot:prepend>
<v-tooltip text="تخفیف مبلغی" location="bottom">
<template v-slot:activator="{ props }">
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
<v-checkbox v-bind="props" v-model="item.showPercentDiscount" hide-details density="compact" color="primary" class="mt-0" style="margin: 0; padding: 0;" @update:modelValue="handleDiscountTypeChange(item)"></v-checkbox>
</template>
</v-tooltip>
</template>
@ -1293,6 +1311,36 @@ export default {
padding-bottom: 8px;
}
:deep(.v-table) {
width: 100%;
table-layout: auto;
}
:deep(.v-table__wrapper) {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
:deep(.v-table__wrapper::-webkit-scrollbar) {
display: none;
}
:deep(.v-table .v-field) {
margin: 4px 0;
}
:deep(.v-table .v-field__input) {
padding: 4px 8px;
}
:deep(.v-table .v-field__outline) {
--v-field-border-width: 1px;
}
:deep(.v-table .v-field--variant-outlined) {
--v-field-border-opacity: 0.12;
}
:deep(.v-overlay__content) {
z-index: 9999 !important;
}

View file

@ -493,6 +493,32 @@
</v-col>
</v-row>
<v-row v-if="isPluginActive('hrm')" class="mt-4">
<v-col cols="12">
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه مدیریت منابع انسانی</v-card-title>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="h-100">
<v-card-text>
<v-list>
<v-list-item>
<v-switch
v-model="info.plugHrmDocs"
label="مدیریت اسناد حقوق و دستمزد"
@change="savePerms('plugHrmDocs')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugHrmDocs"
:disabled="loadingSwitches.plugHrmDocs"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col v-if="isPluginActive('noghre')" cols="12" md="4">
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه کارگاه نقره سازی</v-card-title>
@ -625,7 +651,8 @@ export default {
plugRepservice: false,
plugNoghreAdmin: false,
plugNoghreSell: false,
plugCCAdmin: false
plugCCAdmin: false,
plugHrmDocs: false
};
axios.post('/api/business/get/user/permissions',

View file

@ -1,87 +1,100 @@
<script>
import {defineComponent} from 'vue'
import axios from "axios";
<script setup>
import { ref, onMounted } from 'vue'
import axios from "axios"
export default defineComponent({
name: "plugin-world",
data: ()=>{return{
plugins:{}
}},
methods:{
loadData(){
axios.post('/api/plugin/get/all').then((response)=>{
this.plugins = response.data;
})
}
},
created() {
this.loadData()
}
const plugins = ref([])
const loadData = () => {
axios.post('/api/plugin/get/all').then((response) => {
plugins.value = response.data
})
}
onMounted(() => {
loadData()
})
</script>
<template>
<div class="block block-content-full">
<div id="fixed-header" class="block-header block-header-default bg-gray-light" >
<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-cog"></i>
فهرست افزونهها
</h3>
<div class="block-options">
</div>
</div>
<div class="block-content pb-3">
<div class="container-fluid">
<div class="row">
<div v-for="plugin in plugins" class="col-md-6 col-xl-4">
<!-- House -->
<div class="block block-rounded border">
<div class="block-content p-0 overflow-hidden">
<a class="img-link img-fluid-100" data-action="side_overlay_open" data-toggle="layout" href="javascript:void(0)">
<img alt="" class="img-fluid rounded-top" :src="'/img/plugins/' + plugin.icon">
</a>
</div>
<div class="block-content">
<h4 class="h6 mb-2">
<i class="fa fa-plug-circle-plus"></i>
{{plugin.name}}
</h4>
<h5 class="h2 fw-light push">{{Intl.NumberFormat('en-US').format(plugin.price)}} تومان<br><span class="fs-3 text-muted"><i class="fa fa-clock"></i> {{plugin.timelabel}} </span>
</h5>
</div>
<div class="block-content p-0">
<div class="row text-center m-0 border-top border-bottom bg-body-light">
<div class="col-6 border-end">
<p class="py-3 mb-0">
<i class="fa fa-fw fa-ticket text-muted me-1"></i> پشتیبانی دارد </p>
</div>
<div class="col-6">
<p class="py-3 mb-0">
<i class="fa fa-fw fa-users-rectangle text-muted me-1"></i> کاربر نامحدود </p>
</div>
</div>
</div>
<div class="block-content block-content-full">
<div class="row">
<div class="col-6">
<RouterLink class="btn btn-sm btn-primary w-100" :to="'/acc/plugin-center/view-end/' + plugin.code"> خرید </RouterLink>
</div>
<div class="col-6">
<RouterLink :to="'/acc/plugins/' + plugin.code +'/intro'" class="btn btn-sm btn-alt-primary w-100"> کاتالوگ </RouterLink>
</div>
</div>
</div>
<v-toolbar title="فهرست افزونه‌ها" flat color="toolbar">
<template v-slot:prepend>
<v-btn icon @click="$router.back()" class="me-2 d-none d-md-flex" variant="text">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</v-toolbar>
<v-container fluid>
<v-row>
<v-col
v-for="plugin in plugins"
:key="plugin.code"
cols="12"
md="6"
lg="4"
xl="4"
>
<v-card class="mb-6 elevation-3" rounded="lg">
<v-img
:src="'/u/img/plugins/' + plugin.icon"
height="200"
class="rounded-t-lg"
cover
></v-img>
<v-card-title class="d-flex align-center justify-space-between">
<div>
<v-icon class="me-2" color="primary">mdi-power-plug</v-icon>
<span class="font-weight-bold">{{ plugin.name }}</span>
</div>
<!-- END House -->
</div>
</div>
</div>
</div>
</div>
</v-card-title>
<v-card-subtitle class="d-flex align-center justify-space-between mb-2">
<v-chip color="success" text-color="white" size="small">
{{ Intl.NumberFormat('en-US').format(plugin.price) }} تومان
</v-chip>
<v-chip color="info" text-color="white" size="small">
<v-icon size="small" class="me-1">mdi-clock-outline</v-icon>
{{ plugin.timelabel }}
</v-chip>
</v-card-subtitle>
<v-divider></v-divider>
<v-row class="text-center py-2 bg-grey-lighten-4">
<v-col cols="6" class="border-e">
<v-icon class="me-1" color="grey">mdi-ticket</v-icon>
پشتیبانی دارد
</v-col>
<v-col cols="6">
<v-icon class="me-1" color="grey">mdi-account-group</v-icon>
کاربر نامحدود
</v-col>
</v-row>
<v-divider></v-divider>
<v-card-actions class="justify-center">
<RouterLink :to="'/acc/plugin-center/view-end/' + plugin.code">
<v-btn
color="success"
class="mx-2 px-4"
elevation="1"
variant="flat"
>
<v-icon start class="ms-1">mdi-cart</v-icon>
خرید
</v-btn>
</RouterLink>
<RouterLink :to="'/acc/plugins/' + plugin.code + '/intro'">
<v-btn
color="info"
class="mx-2 px-4"
elevation="1"
variant="outlined"
>
<v-icon start class="ms-1">mdi-book-open-page-variant</v-icon>
کاتالوگ
</v-btn>
</RouterLink>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>

View file

@ -12,13 +12,27 @@ export default defineComponent({
name: '',
price: 0,
timelabel: 0,
icon: '',
},
}),
created() {
axios.post('/api/plugin/get/info/' + this.$route.params.id).then((response) => {
this.item = response.data;
this.loading = false;
});
axios.post('/api/plugin/get/info/' + this.$route.params.id)
.then((response) => {
const data = response.data;
data.id = Number(data.id);
data.price = Number(data.price);
this.item = data;
this.loading = false;
})
.catch((error) => {
this.loading = false;
Swal.fire({
text: 'خطا در دریافت اطلاعات افزونه!',
icon: 'error',
confirmButtonText: 'باشه',
});
console.error(error);
});
},
methods: {
insert() {
@ -51,51 +65,158 @@ export default defineComponent({
<template>
<!-- هدر -->
<v-toolbar color="toolbar" title="جزئیات خرید افزونه">
<v-toolbar title="جزئیات خرید افزونه" flat color="toolbar">
<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>
<v-btn icon @click="$router.back()" class="me-2 d-none d-md-flex" variant="text">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</v-toolbar>
<!-- محتوای اصلی -->
<v-card :loading="loading" elevation="2" class="pa-4 ma-2" color="lighten-5">
<v-card-title class="text-center">
<v-icon size="x-large" color="grey">mdi-toy-brick-outline</v-icon>
</v-card-title>
<v-card-text class="text-center">
<!-- نام افزونه -->
<h3 class="text-h5 text-dark mt-3">{{ item.name }}</h3>
<!-- مدت اعتبار -->
<p class="text-body-1 text-muted mb-3">
مدت اعتبار افزونه:
<span class="text-primary">{{ item.timelabel }}</span>
</p>
<!-- قیمت اصلی -->
<v-chip color="primary" class="mb-3">
{{ formattedPrice }} تومان
</v-chip>
<!-- مبلغ قابل پرداخت -->
<p class="text-body-1 font-weight-bold text-muted">
مبلغ قابل پرداخت (با احتساب مالیات بر ارزش افزوده و کارمزد درگاه واسط):
</p>
<h3 class="text-success">{{ totalPrice }} تومان</h3>
</v-card-text>
<!-- دکمه پرداخت -->
<v-card-actions class="justify-center">
<v-btn variant="tonal" block color="primary" @click="insert">
پرداخت آنلاین از طریق زرینپال
</v-btn>
</v-card-actions>
</v-card>
<v-container class="d-flex justify-center align-center" style="min-height: 80vh;">
<v-row justify="center" align="center">
<v-col cols="12" md="7" lg="5">
<div v-if="loading" class="d-flex flex-column align-center justify-center py-16">
<v-progress-circular indeterminate color="primary" size="48" />
<div class="mt-4 text-body-2 text-muted">در حال بارگذاری اطلاعات...</div>
</div>
<v-card v-else elevation="6" class="pa-7 rounded-xl pay-card clean-card">
<v-img
:src="item.icon ? '/u/img/plugins/' + item.icon : '/images/plugin-default.png'"
height="200"
class="rounded-t-lg plugin-image mb-5"
cover
alt="عکس افزونه"
/>
<v-card-text class="text-center mt-3">
<h2 class="text-h5 font-weight-bold mb-4 main-title">{{ item.name }}</h2>
<div class="mb-4">
<span class="label">مدت اعتبار:</span>
<span class="value">{{ item.timelabel }}</span>
</div>
<v-divider class="my-4" />
<div class="mb-3">
<span class="label">قیمت افزونه:</span>
<span class="value price">{{ formattedPrice }} تومان</span>
</div>
<div class="mb-2">
<span class="label">مبلغ قابل پرداخت (با مالیات و کارمزد):</span>
<span class="value total">{{ totalPrice }} تومان</span>
</div>
</v-card-text>
<v-card-actions class="justify-center mt-8 mb-2 pay-btn-wrapper">
<v-btn
color="success"
size="x-large"
class="rounded-3 px-12 pay-btn"
@click="insert"
prepend-icon="mdi-credit-card-outline"
elevation="1"
>
پرداخت آنلاین
</v-btn>
<v-btn
variant="outlined"
color="grey-darken-2"
size="large"
class="cancel-btn rounded-3"
@click="$router.back()"
prepend-icon="mdi-close-circle-outline"
>
انصراف
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped></style>
<style scoped>
.text-h5 {
font-size: 1.4rem;
}
.pay-card.clean-card {
background: #fff;
border: 1px solid #f1f1f1;
box-shadow: 0 4px 24px 0 rgba(60, 72, 88, 0.08);
min-height: 420px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.main-title {
color: #222;
letter-spacing: -0.5px;
}
.label {
color: #888;
font-size: 1rem;
margin-left: 0.5rem;
}
.value {
color: #222;
font-size: 1.08rem;
font-weight: 500;
}
.price {
color: #1976d2;
font-weight: bold;
margin-right: 0.5rem;
}
.total {
color: #388e3c;
font-weight: bold;
margin-right: 0.5rem;
}
.pay-btn-wrapper {
left: 0;
right: 0;
display: flex;
justify-content: center;
margin-top: 2rem;
margin-bottom: 0.5rem;
}
.pay-btn {
background: #43a047 !important;
color: #fff !important;
font-size: 1.15rem;
font-weight: bold;
box-shadow: 0 2px 8px rgba(67, 160, 71, 0.13);
transition: transform 0.15s, box-shadow 0.15s;
}
.pay-btn:hover {
transform: scale(1.04);
box-shadow: 0 6px 24px rgba(67, 160, 71, 0.18);
}
.cancel-btn {
border: 1px solid #bdbdbd !important;
color: #757575 !important;
background: #fff !important;
margin-right: 1rem;
}
.cancel-btn:hover {
color: #ff9800 !important;
border-color: #ff9800 !important;
}
@media (max-width: 600px) {
.pay-card.clean-card {
min-height: 340px;
padding: 1.5rem !important;
}
.main-title {
font-size: 1.1rem;
}
.pay-btn-wrapper {
bottom: 12px;
}
}
.plugin-image {
display: block;
margin-bottom: 1.5rem;
border-radius: 18px;
box-shadow: 0 2px 12px rgba(60,72,88,0.10);
background: #f8f8f8;
object-fit: cover;
}
</style>

View file

@ -69,6 +69,11 @@
import { getSiteName } from "@/hesabixConfig";
import { onMounted, ref } from "vue";
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: string }>;
}
const installPromptEvent = ref<Event | null>(null);
const browserName = ref<string>("");
const chromeBanner = ref<boolean>(false);

View file

@ -3,7 +3,6 @@
<v-tabs v-model="activeTab" color="primary" grow>
<v-tab value="personal" :text="$t('tabs.personal_info')"></v-tab>
<v-tab value="marketing" :text="$t('tabs.marketing_info')"></v-tab>
<v-tab value="suggestions">{{ $t('tabs.suggestions') }}</v-tab>
</v-tabs>
<v-window v-model="activeTab">
@ -109,15 +108,6 @@
</v-card-text>
</v-card>
</v-window-item>
<!-- تب پیشنهادات -->
<v-window-item value="suggestions">
<v-card class="ma-4">
<v-card-text>
<!-- این بخش فعلاً خالی است -->
</v-card-text>
</v-card>
</v-window-item>
</v-window>
<!-- دیالوگها -->

View file

@ -1,6 +1,6 @@
<template>
<v-system-bar color="primaryLight2">
<v-avatar :image="getbase() + 'img/logo-blue.png'" size="20" class="me-2" />
<v-avatar :image="getbase() + 'u/img/logo-blue.png'" size="20" class="me-2" />
<span>{{ siteSlogan }}</span>
<v-spacer />
</v-system-bar>
@ -10,7 +10,7 @@
{{ siteName }}
</template>
<template v-slot:prepend>
<v-avatar :image="getbase() + 'img/favw.png'" />
<v-avatar :image="getbase() + 'u/img/favw.png'" />
</template>
</v-card>
<v-list class="px-0 pt-0">
@ -68,7 +68,7 @@ import { applicationStore } from "@/stores/applicationStore";
import { useUserStore } from "@/stores/userStore";
import { ref, defineComponent } from "vue";
import { mapActions, mapState, mapStores } from "pinia";
import Change_lang from "/src/components/application/buttons/change_lang.vue";
import Change_lang from "@/components/application/buttons/change_lang.vue";
export default defineComponent({
// eslint-disable-next-line vue/multi-word-component-names,vue/no-reserved-component-names