some bug fix and login with email

This commit is contained in:
Hesabix 2025-11-06 17:36:02 +00:00
parent 9ad415a23b
commit 8d528f70c0
8 changed files with 155 additions and 14 deletions

View file

@ -8,7 +8,6 @@ security:
app_user_provider:
entity:
class: App\Entity\User
property: mobile
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/

View file

@ -1164,6 +1164,7 @@ class SellController extends AbstractController
}
}
$entityManager->flush();
// سیاست جدید: در ویرایش، شماره فاکتور تغییر نمی‌کند (نادیده گرفتن هر ورودی مربوط به شماره)
} else {
// ایجاد فاکتور جدید
$doc = new HesabdariDoc();
@ -1173,7 +1174,36 @@ class SellController extends AbstractController
$doc->setType('sell');
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
// انتخاب شماره فاکتور بر اساس حالت شماره گذاری
$numberingMode = $params['numberingMode'] ?? 'auto';
if ($numberingMode === 'custom') {
$customNumber = isset($params['customNumber']) ? trim((string)$params['customNumber']) : '';
if ($customNumber === '') {
return $this->json([
'result' => 0,
'message' => 'شماره سفارشی ارسال نشده است'
], 400);
}
if (!preg_match('/^[A-Za-z0-9_-]{1,30}$/', $customNumber)) {
return $this->json([
'result' => 0,
'message' => 'فرمت شماره فاکتور نامعتبر است'
], 400);
}
$exist = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $customNumber
]);
if ($exist) {
return $this->json([
'result' => 0,
'message' => 'این شماره فاکتور قبلاً استفاده شده است'
], 400);
}
$doc->setCode($customNumber);
} else {
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
}
if ($TwoStepApproval) {
$doc->setIsPreview(true);
$doc->setIsApproved(false);

View file

@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
/**
* @extends ServiceEntityRepository<User>
@ -17,13 +18,53 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface, UserLoaderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function loadUserByIdentifier(string $identifier): ?User
{
$identifier = trim($identifier);
$qb = $this->createQueryBuilder('u');
if (str_contains($identifier, '@')) {
$email = mb_strtolower($identifier);
return $qb
->andWhere('LOWER(u.email) = :email')
->setParameter('email', $email)
->getQuery()
->getOneOrNullResult();
}
$mobile = $this->normalizeMobile($identifier);
$emailFromIdentifier = mb_strtolower($identifier);
return $qb
->andWhere('u.mobile = :mobile OR LOWER(u.email) = :email')
->setParameter('mobile', $mobile)
->setParameter('email', $emailFromIdentifier)
->getQuery()
->getOneOrNullResult();
}
private function normalizeMobile(string $value): string
{
$value = $this->convertPersianToEnglish($value);
$value = preg_replace('/\s+/', '', $value ?? '');
return (string) $value;
}
private function convertPersianToEnglish(string $input): string
{
$persianNumbers = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
$englishNumbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
return str_replace($persianNumbers, $englishNumbers, $input);
}
public function save(User $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);

View file

@ -46,6 +46,8 @@ const en_lang = {
password_sended: "کلمه عبور جدید به شماره تلفن شما ارسال شد.",
mobile_placeholder:"مثلا 09121234567",
mobile:"Mobile number",
email_or_mobile:"Email or Mobile",
email_or_mobile_placeholder:"Enter your email or mobile number",
send_new_password: "Send new password",
editNumber: "Edit Number",
resendCodeLabel: "Send Again",

View file

@ -683,6 +683,8 @@ const fa_lang = {
password_sended: "کلمه عبور جدید به شماره تلفن شما ارسال شد.",
mobile_placeholder: "مثلا 09121234567",
mobile: "تلفن همراه",
email_or_mobile: "ایمیل یا شماره موبایل",
email_or_mobile_placeholder: "ایمیل یا شماره موبایل خود را وارد کنید",
send_new_password: "ارسال کلمه عبور جدید",
editNumber: "ویرایش شماره",
resendCodeLabel: "ارسال مجدد",

View file

@ -305,8 +305,6 @@ export default {
).then((response) => {
this.items = response.data;
this.items.forEach((item) => {
item.bs = this.$filters.formatNumber(item.bs)
item.bd = this.$filters.formatNumber(item.bd)
item.accounts = []; // Initialize accounts array
})
this.loadCounterpartAccounts(id);

View file

@ -41,6 +41,40 @@
<Hpersonsearch v-model="customer" label="خریدار" :rules="[v => !!v || 'خریدار الزامی است']" required></Hpersonsearch>
</v-col>
</v-row>
<v-row class="mb-2" v-if="isNewInvoice">
<v-col cols="12" sm="6">
<v-radio-group v-model="numberingMode" inline density="compact">
<v-label class="mb-2 d-inline-block">روش شمارهگذاری</v-label>
<v-radio label="خودکار" value="auto"></v-radio>
<v-radio label="سفارشی" value="custom"></v-radio>
</v-radio-group>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-if="numberingMode === 'custom'"
v-model="customNumber"
label="شماره فاکتور (سفارشی)"
density="compact"
hide-details
:counter="30"
:rules="[
v => !!v || 'شماره سفارشی الزامی است',
v => /^[A-Za-z0-9_-]{1,30}$/.test(v || '') || 'فقط حروف/اعداد/خط تیره/زیرخط، حداکثر ۳۰ کاراکتر'
]"
></v-text-field>
</v-col>
</v-row>
<v-row class="mb-2" v-else>
<v-col cols="12" sm="6">
<v-text-field
v-model="invoiceNumber"
label="شماره فاکتور"
density="compact"
hide-details
readonly
></v-text-field>
</v-col>
</v-row>
<v-text-field v-model="invoiceDescription" label="توضیحات فاکتور" density="compact" hide-details class="mb-4">
<template v-slot:prepend-inner>
<mostdes v-model="invoiceDescription" :submitData="{ id: null, des: invoiceDescription }" type="sell" label=""></mostdes>
@ -624,6 +658,9 @@ export default {
const hasChanges = ref(false);
const isNewInvoice = ref(true);
const isInitializing = ref(true);
const numberingMode = ref('auto');
const customNumber = ref('');
const invoiceNumber = ref('');
// بارگذاری تنظیمات از لوکال استوریج
const loadSettings = () => {
@ -710,7 +747,10 @@ export default {
showDraftDialog,
hasChanges,
isNewInvoice,
isInitializing
isInitializing,
numberingMode,
customNumber,
invoiceNumber
};
},
watch: {
@ -893,6 +933,14 @@ export default {
});
}
if (this.isNewInvoice && this.numberingMode === 'custom') {
if (!this.customNumber || this.customNumber.trim() === '') {
this.validationErrors.push('شماره سفارشی الزامی است');
} else if (!/^[A-Za-z0-9_-]{1,30}$/.test(this.customNumber)) {
this.validationErrors.push('شماره سفارشی فقط حروف/اعداد/خط تیره/زیرخط و حداکثر ۳۰ کاراکتر');
}
}
if (this.validationErrors.length > 0) {
this.showValidationErrors = true;
}
@ -913,6 +961,14 @@ export default {
this.invoiceDate = data.date;
this.customer = data.person.id;
this.invoiceDescription = data.des;
// مقدار شماره فاکتور برای نمایش فقط خواندنی (code یا در نبود آن id)
{
let codeField = (data.code !== undefined && data.code !== null) ? data.code : (data.id !== undefined && data.id !== null ? data.id : '');
if (codeField === '' || codeField === null || codeField === undefined) {
codeField = this.$route.params.id || '';
}
this.invoiceNumber = codeField !== '' ? String(codeField) : '';
}
this.taxPercent = data.taxPercent;
this.totalDiscount = Number(data.totalDiscount);
this.totalDiscountPercent = Number(data.discountPercent);
@ -985,6 +1041,10 @@ export default {
totalDiscount: this.showTotalPercentDiscount ? 0 : this.totalDiscount,
shippingCost: this.shippingCost,
showTotalPercentDiscount: this.showTotalPercentDiscount,
...(this.isNewInvoice ? {
numberingMode: this.numberingMode,
customNumber: this.numberingMode === 'custom' ? this.customNumber.trim() : null
} : {}),
items: this.items.map(item => ({
name: {
id: item.name.id,

View file

@ -10,9 +10,9 @@
<v-form :disabled="loading" ref="form" fast-fail @submit.prevent="submit()">
<v-card-text>
<v-text-field class="mb-2" :label="$t('user.mobile')" :placeholder="$t('user.mobile_placeholder')"
single-line v-model="user.mobile" type="tel" variant="outlined" prepend-inner-icon="mdi-phone"
:rules="rules.mobile" autocomplete="tel" name="mobile"></v-text-field>
<v-text-field class="mb-2" :label="$t('user.email_or_mobile')" :placeholder="$t('user.email_or_mobile_placeholder')"
single-line v-model="user.mobile" type="text" variant="outlined" prepend-inner-icon="mdi-account"
:rules="rules.identifier" autocomplete="username" name="identifier"></v-text-field>
<v-text-field class="mb-2" :label="$t('user.password')" :placeholder="$t('user.password_placeholder')"
single-line type="password" variant="outlined" prepend-inner-icon="mdi-lock" :rules="rules.password"
@ -88,8 +88,8 @@ export default {
standard: true,
},
rules: {
mobile: [
(value: any) => self.validate(value, 'mobile'),
identifier: [
(value: any) => self.validate(value, 'identifier'),
],
password: [
(value: any) => self.validate(value, "password"),
@ -102,8 +102,13 @@ export default {
},
methods: {
validate(input: string, type: string) {
if (type === "mobile") {
const normalizedInput = this.convertPersianToEnglish(input.replace(/\s/g, ''));
if (type === "identifier") {
const raw = (input || '').toString().trim();
if (raw.includes('@')) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(raw) || this.$t("validator.email_not_valid");
}
const normalizedInput = this.convertPersianToEnglish(raw.replace(/\s/g, ''));
if (/^09\d{9}$/.test(normalizedInput)) return true;
return this.$t("validator.mobile_not_valid");
} else if (type === "password") {
@ -141,8 +146,12 @@ export default {
if (valid) {
this.loading = true;
const raw = (this.user.mobile || '').toString().trim();
const isEmail = raw.includes('@');
const identifier = isEmail ? raw.toLowerCase() : this.convertPersianToEnglish(raw.replace(/\s/g, ''));
const userData: { mobile: string; password: string; standard: boolean; captcha_answer?: string } = {
mobile: this.convertPersianToEnglish(this.user.mobile.replace(/\s/g, '')),
mobile: identifier,
password: this.user.password,
standard: this.user.standard,
};