first commit
This commit is contained in:
commit
b71ea45681
898 changed files with 138202 additions and 0 deletions
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
32
accounts/admin.py
Normal file
32
accounts/admin.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from accounts.models import Role, Profile
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'parent', 'is_active']
|
||||
search_fields = ['name', 'slug']
|
||||
list_filter = ['is_active']
|
||||
ordering = ['parent__name', 'name']
|
||||
|
||||
@admin.register(Profile)
|
||||
class ProfileAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"user",
|
||||
"fullname",
|
||||
"pic_tag",
|
||||
"roles_str",
|
||||
"affairs",
|
||||
"county",
|
||||
"broker",
|
||||
"is_completed",
|
||||
"is_active",
|
||||
"jcreated",
|
||||
]
|
||||
search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number']
|
||||
list_filter = ['user', 'roles', 'affairs', 'county', 'broker']
|
||||
date_hierarchy = 'created'
|
||||
ordering = ['-created']
|
||||
readonly_fields = ['created', 'updated']
|
7
accounts/apps.py
Normal file
7
accounts/apps.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
||||
verbose_name = "حسابها"
|
157
accounts/forms.py
Normal file
157
accounts/forms.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from .models import Profile, Role
|
||||
from common.consts import UserRoles
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class CustomerForm(forms.ModelForm):
|
||||
"""فرم برای افزودن مشترک جدید"""
|
||||
first_name = forms.CharField(
|
||||
max_length=150,
|
||||
label="نام",
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'نام'
|
||||
})
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
max_length=150,
|
||||
label="نام خانوادگی",
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'نام خانوادگی'
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = [
|
||||
'phone_number_1', 'phone_number_2', 'national_code',
|
||||
'address', 'card_number', 'account_number'
|
||||
]
|
||||
widgets = {
|
||||
'phone_number_1': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '09123456789'
|
||||
}),
|
||||
'phone_number_2': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '02112345678'
|
||||
}),
|
||||
'national_code': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '1234567890',
|
||||
'maxlength': '10',
|
||||
'required': 'required'
|
||||
}),
|
||||
'address': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'آدرس کامل',
|
||||
'rows': '3'
|
||||
}),
|
||||
'card_number': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'شماره کارت بانکی',
|
||||
'maxlength': '16'
|
||||
}),
|
||||
'account_number': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'شماره حساب بانکی',
|
||||
'maxlength': '20'
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'phone_number_1': 'تلفن ۱',
|
||||
'phone_number_2': 'تلفن ۲',
|
||||
'national_code': 'کد ملی',
|
||||
'address': 'آدرس',
|
||||
'card_number': 'شماره کارت',
|
||||
'account_number': 'شماره حساب',
|
||||
}
|
||||
|
||||
def clean_national_code(self):
|
||||
national_code = self.cleaned_data.get('national_code')
|
||||
if national_code:
|
||||
# Check if user with this national code exists
|
||||
existing_user = User.objects.filter(username=national_code).first()
|
||||
if existing_user:
|
||||
# If we're editing and the user is the same, it's OK
|
||||
if self.instance and self.instance.user and existing_user == self.instance.user:
|
||||
return national_code
|
||||
# Otherwise, it's a duplicate
|
||||
raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.')
|
||||
return national_code
|
||||
|
||||
def save(self, commit=True):
|
||||
# Check if this is an update (instance exists)
|
||||
if self.instance and self.instance.pk:
|
||||
# Update existing profile
|
||||
profile = super().save(commit=False)
|
||||
|
||||
# Update user information
|
||||
user = profile.user
|
||||
user.first_name = self.cleaned_data['first_name']
|
||||
user.last_name = self.cleaned_data['last_name']
|
||||
user.save()
|
||||
|
||||
# Set affairs, county, and broker from current user's profile
|
||||
if hasattr(self, 'request') and self.request.user.is_authenticated:
|
||||
current_user_profile = getattr(self.request.user, 'profile', None)
|
||||
if current_user_profile:
|
||||
profile.affairs = current_user_profile.affairs
|
||||
profile.county = current_user_profile.county
|
||||
profile.broker = current_user_profile.broker
|
||||
|
||||
if commit:
|
||||
profile.save()
|
||||
self.save_m2m()
|
||||
|
||||
return profile
|
||||
else:
|
||||
# Create new profile
|
||||
# Get national code as username
|
||||
national_code = self.cleaned_data.get('national_code')
|
||||
if not national_code:
|
||||
raise forms.ValidationError('کد ملی الزامی است.')
|
||||
|
||||
# Create User with default password
|
||||
user = User.objects.create_user(
|
||||
username=national_code,
|
||||
email='', # Empty email
|
||||
password='sooha1234', # Default password
|
||||
first_name=self.cleaned_data['first_name'],
|
||||
last_name=self.cleaned_data['last_name']
|
||||
)
|
||||
|
||||
# Create Profile
|
||||
profile = super().save(commit=False)
|
||||
profile.user = user
|
||||
profile.owner = user
|
||||
|
||||
# Set affairs, county, and broker from current user's profile
|
||||
if hasattr(self, 'request') and self.request.user.is_authenticated:
|
||||
current_user_profile = getattr(self.request.user, 'profile', None)
|
||||
if current_user_profile:
|
||||
profile.affairs = current_user_profile.affairs
|
||||
profile.county = current_user_profile.county
|
||||
profile.broker = current_user_profile.broker
|
||||
|
||||
if commit:
|
||||
profile.save()
|
||||
self.save_m2m()
|
||||
|
||||
# Add customer role after profile is saved
|
||||
customer_role = Role.objects.filter(slug=UserRoles.CUSTOMER.value).first()
|
||||
if customer_role:
|
||||
profile.roles.add(customer_role)
|
||||
else:
|
||||
# Create customer role if it doesn't exist
|
||||
customer_role = Role.objects.create(
|
||||
name='مشترک',
|
||||
slug=UserRoles.CUSTOMER.value
|
||||
)
|
||||
profile.roles.add(customer_role)
|
||||
|
||||
return profile
|
1
accounts/management/__init__.py
Normal file
1
accounts/management/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Management package for accounts app
|
1
accounts/management/commands/__init__.py
Normal file
1
accounts/management/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Commands package for accounts app
|
51
accounts/management/commands/create_roles.py
Normal file
51
accounts/management/commands/create_roles.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from accounts.models import Role
|
||||
from common.consts import UserRoles
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generates default roles"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
roles = [
|
||||
{
|
||||
"name": "ادمین",
|
||||
"slug": UserRoles.ADMIN,
|
||||
},
|
||||
{
|
||||
"name": "مشترک",
|
||||
"slug": UserRoles.CUSTOMER,
|
||||
},
|
||||
{
|
||||
"name": "مدیر",
|
||||
"slug": UserRoles.MANAGER,
|
||||
},
|
||||
{
|
||||
"name": "حسابدار",
|
||||
"slug": UserRoles.ACCOUNTANT,
|
||||
},
|
||||
{
|
||||
"name": "پیشخوان",
|
||||
"slug": UserRoles.BROKER,
|
||||
},
|
||||
{
|
||||
"name": "نصاب",
|
||||
"slug": UserRoles.INSTALLER,
|
||||
},
|
||||
{
|
||||
"name": "کارشناس امور",
|
||||
"slug": UserRoles.REGIONAL_WATER_AUTHORITY,
|
||||
},
|
||||
{
|
||||
"name": "مدیر منابع آب",
|
||||
"slug": UserRoles.WATER_RESOURCE_MANAGER,
|
||||
},
|
||||
{
|
||||
"name": "ستاد آبمنطقهای",
|
||||
"slug": UserRoles.HEADQUARTER,
|
||||
},
|
||||
]
|
||||
|
||||
for role in roles:
|
||||
Role.objects.get_or_create(name=role['name'], slug=role['slug'].value)
|
62
accounts/migrations/0001_initial.py
Normal file
62
accounts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-07 09:08
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
|
||||
('name', models.CharField(max_length=100, verbose_name='نام')),
|
||||
('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='accounts.role', verbose_name='زیرشاخه')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'نقش',
|
||||
'verbose_name_plural': 'نقش\u200cها',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
|
||||
('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
|
||||
('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
|
||||
('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
|
||||
('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
|
||||
('pic', models.ImageField(default='../static/dist/img/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
|
||||
('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
|
||||
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_profiles', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
|
||||
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
|
||||
('roles', models.ManyToManyField(blank=True, related_name='profiles', to='accounts.role', verbose_name='نقش\u200cها')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'پروفایل',
|
||||
'verbose_name_plural': 'پروفایل\u200cها',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,75 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-07 14:29
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
('locations', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='affairs',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='broker',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='county',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='pic',
|
||||
field=models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalProfile',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
|
||||
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
|
||||
('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
|
||||
('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
|
||||
('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
|
||||
('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
|
||||
('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
|
||||
('pic', models.TextField(default='../static/sample_images/profile.jpg', max_length=100, verbose_name='تصویر')),
|
||||
('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
|
||||
('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
|
||||
('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
|
||||
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical پروفایل',
|
||||
'verbose_name_plural': 'historical پروفایل\u200cها',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
]
|
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
172
accounts/models.py
Normal file
172
accounts/models.py
Normal file
|
@ -0,0 +1,172 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
from django.core.validators import RegexValidator
|
||||
from simple_history.models import HistoricalRecords
|
||||
from common.models import TagModel, BaseModel
|
||||
from common.consts import UserRoles
|
||||
from locations.models import Affairs, Broker, County
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Role(TagModel):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "نقش"
|
||||
verbose_name_plural = "نقشها"
|
||||
|
||||
|
||||
class Profile(BaseModel):
|
||||
user = models.OneToOneField(
|
||||
get_user_model(),
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="کاربر",
|
||||
related_name="profile"
|
||||
)
|
||||
national_code = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
verbose_name="کد ملی",
|
||||
blank=True,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r'^\d+$',
|
||||
message='کد ملی باید فقط شامل اعداد باشد.',
|
||||
code='invalid_national_code'
|
||||
)
|
||||
]
|
||||
)
|
||||
address = models.TextField(
|
||||
null=True,
|
||||
verbose_name="آدرس",
|
||||
blank=True
|
||||
)
|
||||
card_number = models.CharField(
|
||||
max_length=16,
|
||||
null=True,
|
||||
verbose_name="شماره کارت",
|
||||
blank=True,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r'^\d+$',
|
||||
message='شماره کارت باید فقط شامل اعداد باشد.',
|
||||
code='invalid_card_number'
|
||||
)
|
||||
]
|
||||
)
|
||||
account_number = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
verbose_name="شماره حساب",
|
||||
blank=True,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r'^\d+$',
|
||||
message='شماره حساب باید فقط شامل اعداد باشد.',
|
||||
code='invalid_account_number'
|
||||
)
|
||||
]
|
||||
)
|
||||
phone_number_1 = models.CharField(
|
||||
max_length=11,
|
||||
null=True,
|
||||
verbose_name="شماره تماس ۱",
|
||||
blank=True
|
||||
)
|
||||
phone_number_2 = models.CharField(
|
||||
max_length=11,
|
||||
null=True,
|
||||
verbose_name="شماره تماس ۲",
|
||||
blank=True
|
||||
)
|
||||
|
||||
pic = models.ImageField(
|
||||
upload_to="profile_images",
|
||||
verbose_name="تصویر",
|
||||
default="../static/sample_images/profile.jpg"
|
||||
)
|
||||
roles = models.ManyToManyField(
|
||||
Role,
|
||||
verbose_name="نقشها",
|
||||
blank=True,
|
||||
related_name="profiles"
|
||||
)
|
||||
is_completed = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="پروفایل تکمیل شده"
|
||||
)
|
||||
owner = models.ForeignKey(
|
||||
get_user_model(),
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="ایجاد کننده",
|
||||
blank=True,
|
||||
related_name="owned_profiles"
|
||||
)
|
||||
affairs = models.ForeignKey(
|
||||
Affairs,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="امور",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
county = models.ForeignKey(
|
||||
County,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="شهرستان",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
broker = models.ForeignKey(
|
||||
Broker,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="کارگزار",
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "پروفایل"
|
||||
verbose_name_plural = "پروفایلها"
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
|
||||
def first_name(self):
|
||||
return self.user.first_name
|
||||
|
||||
first_name.short_description = "نام"
|
||||
|
||||
def last_name(self):
|
||||
return self.user.last_name
|
||||
|
||||
last_name.short_description = "نام خانوادگی"
|
||||
|
||||
def fullname(self):
|
||||
return self.user.get_full_name()
|
||||
|
||||
fullname.short_description = "نام و نام خانوادگی"
|
||||
|
||||
def roles_str(self):
|
||||
return ', '.join([role.name for role in self.roles.all()])
|
||||
|
||||
roles_str.short_description = "نقشها"
|
||||
|
||||
def has_role(self, role: UserRoles):
|
||||
return self.roles.filter(slug=role.value).exists()
|
||||
|
||||
def has_any_of(self, roles: list[UserRoles]):
|
||||
allowed_roles = [role.value for role in roles]
|
||||
return len(self.roles.filter(slug__in=allowed_roles)) > 0
|
||||
|
||||
def has_none_of(self, roles: list[UserRoles]):
|
||||
return not self.has_any_of(roles)
|
||||
|
||||
def pic_tag(self):
|
||||
return format_html(f"<img style='width:30px;' src='{self.pic.url}'>")
|
||||
|
||||
pic_tag.short_description = "تصویر"
|
489
accounts/templates/accounts/customer_list.html
Normal file
489
accounts/templates/accounts/customer_list.html
Normal file
|
@ -0,0 +1,489 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include 'sidebars/admin.html' %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include 'navbars/admin.html' %}
|
||||
{% endblock navbar %}
|
||||
|
||||
|
||||
{% block title %}کاربرها{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-bs5/datatables.bootstrap5.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.css' %}">
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Include Toasts -->
|
||||
{% include '_toasts.html' %}
|
||||
|
||||
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
|
||||
<div class="row py-3 mb-4 card-header flex-column flex-md-row pb-0">
|
||||
<div class="d-md-flex justify-content-between align-items-center dt-layout-start col-md-auto me-auto mt-0">
|
||||
<h5 class="card-title mb-0 text-md-start text-center fw-bold">لیست کاربرها</h5>
|
||||
</div>
|
||||
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
|
||||
<div class="dt-buttons btn-group flex-wrap mb-0">
|
||||
<div class="btn-group">
|
||||
<button class="btn buttons-collection btn-label-primary dropdown-toggle me-4" tabindex="0" aria-controls="DataTables_Table_0" type="button" aria-haspopup="dialog" aria-expanded="false">
|
||||
<span>
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<i class="icon-base bx bx-export me-sm-1">
|
||||
</i>
|
||||
<span class="d-none d-sm-inline-block">
|
||||
خروجی
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-primary " onclick="prepareAddForm()">
|
||||
<i class="bx bx-plus me-1"></i>
|
||||
افزودن کاربر جدید
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-datatable table-responsive">
|
||||
<table class="datatables-basic table border-top" id="customers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کاربر</th>
|
||||
<th>کد ملی</th>
|
||||
<th>تلفن</th>
|
||||
<th>آدرس</th>
|
||||
<th>وضعیت</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for customer in customers %}
|
||||
<tr>
|
||||
<td>{{forloop.counter}}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-start align-items-center user-name">
|
||||
<div class="avatar-wrapper">
|
||||
<div class="avatar me-2">
|
||||
{% if customer.pic and customer.pic.url %}
|
||||
<img src="{{ customer.pic.url }}" alt="Avatar" class="rounded-circle" style="width: 40px; height: 40px; object-fit: cover;">
|
||||
{% else %}
|
||||
<span class="avatar-initial rounded-circle bg-label-primary">
|
||||
{% if customer.user.first_name and customer.user.last_name %}
|
||||
{{ customer.user.first_name|first|upper }}{{ customer.user.last_name|first|upper }}
|
||||
{% elif customer.user.first_name %}
|
||||
{{ customer.user.first_name|first|upper }}
|
||||
{% elif customer.user.last_name %}
|
||||
{{ customer.user.last_name|first|upper }}
|
||||
{% else %}
|
||||
{{ customer.user.username|first|upper }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="emp_name text-truncate text-heading">{{ customer.user.get_full_name|default:customer.user.username }}</span>
|
||||
<small class="emp_post text-truncate">{{ customer.user.username }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ customer.national_code|default:"کد ملی ثبت نشده" }}</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
{% if customer.phone_number_1 %}
|
||||
<span class="fw-medium">{{ customer.phone_number_1 }}</span>
|
||||
{% endif %}
|
||||
{% if customer.phone_number_2 %}
|
||||
<small class="text-muted">{{ customer.phone_number_2 }}</small>
|
||||
{% endif %}
|
||||
{% if not customer.phone_number_1 and not customer.phone_number_2 %}
|
||||
<span class="text-muted">تلفن ثبت نشده</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if customer.address %}
|
||||
<span class="text-truncate d-inline-block" style="max-width: 200px;" title="{{ customer.address }}">
|
||||
{{ customer.address|truncatechars:50 }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted">آدرس ثبت نشده</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if customer.is_completed %}
|
||||
<span class="badge bg-label-success">تکمیل شده</span>
|
||||
{% else %}
|
||||
<span class="badge bg-label-warning">ناقص</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-inline-block">
|
||||
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
|
||||
<i class="icon-base bx bx-dots-vertical-rounded"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end m-0">
|
||||
<li>
|
||||
<a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="viewCustomer(this.getAttribute('data-customer-id'))">
|
||||
<i class="bx bx-show me-1"></i>مشاهده جزئیات
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
|
||||
<i class="bx bx-edit me-1"></i>ویرایش
|
||||
</a>
|
||||
</li>
|
||||
<div class="dropdown-divider"></div>
|
||||
<li>
|
||||
<a href="#" class="dropdown-item text-danger" data-customer-id="{{ customer.id }}" data-customer-name="{{ customer.user.get_full_name|default:customer.user.username }}" onclick="deleteCustomer(this.getAttribute('data-customer-id'), this.getAttribute('data-customer-name'))">
|
||||
<i class="bx bx-trash me-1"></i>حذف
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#" class="btn btn-icon item-edit" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
|
||||
<i class="icon-base bx bx-edit icon-sm"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<i class="bx bx-user-x bx-lg text-muted mb-2"></i>
|
||||
<span class="text-muted">هیچ کاربری یافت نشد</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal to add/edit record -->
|
||||
<div class="offcanvas offcanvas-end" id="add-new-record">
|
||||
<div class="offcanvas-header border-bottom">
|
||||
<h5 class="offcanvas-title" id="exampleModalLabel">افزودن کاربر جدید</h5>
|
||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body flex-grow-1">
|
||||
<form class="add-new-record pt-0 row g-2" id="form-add-new-record" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="customer-id" name="customer_id" value="">
|
||||
|
||||
<!-- User Information -->
|
||||
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-bold" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-user"></i></span>
|
||||
{{ form.first_name }}
|
||||
</div>
|
||||
{% if form.first_name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.first_name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-bold" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-user"></i></span>
|
||||
{{ form.last_name }}
|
||||
</div>
|
||||
{% if form.last_name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.last_name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-bold" for="{{ form.phone_number_1.id_for_label }}">{{ form.phone_number_1.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-phone"></i></span>
|
||||
{{ form.phone_number_1 }}
|
||||
</div>
|
||||
{% if form.phone_number_1.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.phone_number_1.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label fw-bold" for="{{ form.phone_number_2.id_for_label }}">{{ form.phone_number_2.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-phone"></i></span>
|
||||
{{ form.phone_number_2 }}
|
||||
</div>
|
||||
{% if form.phone_number_2.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.phone_number_2.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label fw-bold" for="{{ form.national_code.id_for_label }}">{{ form.national_code.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-id-card"></i></span>
|
||||
{{ form.national_code }}
|
||||
</div>
|
||||
{% if form.national_code.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.national_code.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label fw-bold" for="{{ form.card_number.id_for_label }}">{{ form.card_number.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-credit-card"></i></span>
|
||||
{{ form.card_number }}
|
||||
</div>
|
||||
{% if form.card_number.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.card_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label fw-bold" for="{{ form.account_number.id_for_label }}">{{ form.account_number.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-credit-card"></i></span>
|
||||
{{ form.account_number }}
|
||||
</div>
|
||||
{% if form.account_number.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.account_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="form-label fw-bold" for="{{ form.address.id_for_label }}">{{ form.address.label }}</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text"><i class="bx bx-map"></i></span>
|
||||
{{ form.address }}
|
||||
</div>
|
||||
{% if form.address.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.address.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-primary data-submit me-sm-3 me-1">ذخیره</button>
|
||||
<button type="reset" class="btn btn-outline-secondary" data-bs-dismiss="offcanvas">انصراف</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteConfirmModalLabel">تایید حذف</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="deleteConfirmText">آیا از حذف این کاربر اطمینان دارید؟</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<!-- DataTables JS -->
|
||||
<script src="{% static 'assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js' %}"></script>
|
||||
<!-- Persian DataTable Language -->
|
||||
<script src="{% static 'assets/js/persian-datatable.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with Persian language
|
||||
$('#customers-table').DataTable({
|
||||
pageLength: 10,
|
||||
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
|
||||
order: [[0, 'asc']],
|
||||
responsive: true,
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
$('#form-add-new-record').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = this;
|
||||
const formData = new FormData(form);
|
||||
const customerId = $('#customer-id').val();
|
||||
|
||||
// Determine URL based on whether we're editing or adding
|
||||
const url = customerId ? '{% url "accounts:edit_customer_ajax" 0 %}'.replace('0', customerId) : '{% url "accounts:add_customer_ajax" %}';
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = $(form).find('button[type="submit"]');
|
||||
const originalText = submitBtn.text();
|
||||
submitBtn.prop('disabled', true).text('در حال ذخیره...');
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
showToast(response.message, 'success');
|
||||
|
||||
// Close offcanvas and reset form
|
||||
$('#add-new-record').offcanvas('hide');
|
||||
form.reset();
|
||||
|
||||
// Reload page to show new customer
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
// Show error message
|
||||
showToast(response.message, 'danger');
|
||||
|
||||
// Show form errors if any
|
||||
if (response.errors) {
|
||||
Object.keys(response.errors).forEach(function(field) {
|
||||
const errorMsg = response.errors[field][0];
|
||||
const fieldElement = $('[name="' + field + '"]');
|
||||
fieldElement.addClass('is-invalid');
|
||||
fieldElement.siblings('.invalid-feedback').remove();
|
||||
fieldElement.after('<div class="invalid-feedback d-block">' + errorMsg + '</div>');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// Show error message
|
||||
showToast('خطا در ارتباط با سرور', 'danger');
|
||||
},
|
||||
complete: function() {
|
||||
// Reset button state
|
||||
submitBtn.prop('disabled', false).text(originalText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reset form when offcanvas is hidden
|
||||
$('#add-new-record').on('hidden.bs.offcanvas', function() {
|
||||
const form = $('#form-add-new-record')[0];
|
||||
form.reset();
|
||||
|
||||
// Reset form for adding new customer
|
||||
$('#customer-id').val('');
|
||||
$('#exampleModalLabel').text('افزودن کاربر جدید');
|
||||
$('.data-submit').text('ذخیره');
|
||||
|
||||
// Clear validation errors
|
||||
$('.is-invalid').removeClass('is-invalid');
|
||||
$('.invalid-feedback').remove();
|
||||
});
|
||||
|
||||
// Clear validation errors when user starts typing
|
||||
$('input, textarea').on('input', function() {
|
||||
$(this).removeClass('is-invalid');
|
||||
$(this).siblings('.invalid-feedback').remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Customer functions
|
||||
function viewCustomer(id) {
|
||||
// Implement view functionality
|
||||
console.log('View customer:', id);
|
||||
}
|
||||
|
||||
function editCustomer(id) {
|
||||
// Load customer data and open edit form
|
||||
$.ajax({
|
||||
url: '{% url "accounts:get_customer_data" 0 %}'.replace('0', id),
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
const customer = response.customer;
|
||||
|
||||
// Fill form with customer data
|
||||
const fieldsMap = {
|
||||
'customer-id': customer.id,
|
||||
'id_first_name': customer.first_name,
|
||||
'id_last_name': customer.last_name,
|
||||
'id_phone_number_1': customer.phone_number_1,
|
||||
'id_phone_number_2': customer.phone_number_2,
|
||||
'id_national_code': customer.national_code,
|
||||
'id_card_number': customer.card_number,
|
||||
'id_account_number': customer.account_number,
|
||||
'id_address': customer.address
|
||||
};
|
||||
|
||||
// Loop through fields for easier maintenance
|
||||
Object.keys(fieldsMap).forEach(function(fieldId) {
|
||||
$('#' + fieldId).val(fieldsMap[fieldId] || '');
|
||||
});
|
||||
|
||||
// Update modal title and button
|
||||
$('#exampleModalLabel').text('ویرایش کاربر');
|
||||
$('.data-submit').text('ویرایش');
|
||||
|
||||
// Open modal
|
||||
$('#add-new-record').offcanvas('show');
|
||||
} else {
|
||||
showToast('خطا در بارگذاری اطلاعات کاربر', 'danger');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showToast('خطا در ارتباط با سرور', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCustomer(id, name) {
|
||||
// Set modal content
|
||||
document.getElementById('deleteConfirmText').textContent = `آیا از حذف کاربر "${name}" اطمینان دارید؟`;
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
|
||||
modal.show();
|
||||
|
||||
// Handle confirm button click
|
||||
document.getElementById('confirmDeleteBtn').onclick = function() {
|
||||
// Implement delete functionality
|
||||
console.log('Delete customer:', id);
|
||||
showToast('کاربر با موفقیت حذف شد.', 'success');
|
||||
modal.hide();
|
||||
};
|
||||
}
|
||||
|
||||
function prepareAddForm() {
|
||||
// Reset form for adding new customer
|
||||
const form = $('#form-add-new-record')[0];
|
||||
form.reset();
|
||||
$('#customer-id').val('');
|
||||
$('#exampleModalLabel').text('افزودن کاربر جدید');
|
||||
$('.data-submit').text('ذخیره');
|
||||
|
||||
// Clear validation errors
|
||||
$('.is-invalid').removeClass('is-invalid');
|
||||
$('.invalid-feedback').remove();
|
||||
|
||||
// Open modal
|
||||
$('#add-new-record').offcanvas('show');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
1072
accounts/templates/accounts/dashboard.html
Normal file
1072
accounts/templates/accounts/dashboard.html
Normal file
File diff suppressed because it is too large
Load diff
2849
accounts/templates/accounts/index.html
Normal file
2849
accounts/templates/accounts/index.html
Normal file
File diff suppressed because it is too large
Load diff
137
accounts/templates/accounts/login.html
Normal file
137
accounts/templates/accounts/login.html
Normal file
|
@ -0,0 +1,137 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block layout %}
|
||||
layout-wide customizer-hide
|
||||
{% endblock layout %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
ورود
|
||||
{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static 'assets/vendor/css/pages/page-auth.css' %}">
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xxl">
|
||||
<div class="authentication-wrapper authentication-basic container-p-y">
|
||||
<div class="authentication-inner">
|
||||
<!-- Register -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<!-- Logo -->
|
||||
<div class="app-brand justify-content-center">
|
||||
<a href="index.html" class="app-brand-link gap-2">
|
||||
<span class="app-brand-logo demo">
|
||||
|
||||
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
|
||||
<path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
|
||||
<path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
|
||||
<path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
|
||||
</defs>
|
||||
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
|
||||
<g id="Icon" transform="translate(27.000000, 15.000000)">
|
||||
<g id="Mask" transform="translate(0.000000, 8.000000)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use fill="#696cff" xlink:href="#path-1"></use>
|
||||
<g id="Path-3" mask="url(#mask-2)">
|
||||
<use fill="#696cff" xlink:href="#path-3"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="Path-4" mask="url(#mask-2)">
|
||||
<use fill="#696cff" xlink:href="#path-4"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
|
||||
<use fill="#696cff" xlink:href="#path-5"></use>
|
||||
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- /Logo -->
|
||||
<h4 class="mb-2">Welcome to Sneat! 👋</h4>
|
||||
<p class="mb-4">Please sign-in to your account and start the adventure</p>
|
||||
|
||||
<form id="formAuthentication" class="mb-3 fv-plugins-bootstrap5 fv-plugins-framework" method="post" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3 fv-plugins-icon-container">
|
||||
<label for="email" class="form-label">Email or Username</label>
|
||||
<input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
|
||||
<div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
|
||||
<div class="mb-3 form-password-toggle fv-plugins-icon-container">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<a href="auth-forgot-password-basic.html">
|
||||
<small>Forgot Password?</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="input-group input-group-merge has-validation">
|
||||
<input type="password" id="password" class="form-control" name="password" placeholder="············" aria-describedby="password">
|
||||
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
|
||||
</div><div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="remember-me">
|
||||
<label class="form-check-label" for="remember-me">
|
||||
Remember Me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-primary d-grid w-100" type="submit">Sign in</button>
|
||||
</div>
|
||||
<input type="hidden"></form>
|
||||
|
||||
<p class="text-center">
|
||||
<span>New on our platform?</span>
|
||||
<a href="auth-register-basic.html">
|
||||
<span>Create an account</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="divider my-4">
|
||||
<div class="divider-text">or</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="javascript:;" class="btn btn-icon btn-label-facebook me-3">
|
||||
<i class="tf-icons bx bxl-facebook"></i>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="btn btn-icon btn-label-google-plus me-3">
|
||||
<i class="tf-icons bx bxl-google-plus"></i>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="btn btn-icon btn-label-twitter">
|
||||
<i class="tf-icons bx bxl-twitter"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /Register -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'assets/js/pages-auth.js' %}"></script>
|
||||
{% endblock scripts %}
|
60
accounts/templatetags/accounts_tags.py
Normal file
60
accounts/templatetags/accounts_tags.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from django import template
|
||||
|
||||
from common.consts import UserRoles
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def has_item(list, item):
|
||||
return item in list
|
||||
|
||||
|
||||
def _has_profile(user):
|
||||
return hasattr(user, "profile") and user.profile
|
||||
|
||||
|
||||
@register.filter
|
||||
def has_role(user, role_slugs):
|
||||
return _has_profile(user) and len(user.profile.roles.filter(slug__in=role_slugs.split(","))) > 0
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_admin(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.ADMIN)
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_manager(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.MANAGER)
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_accountant(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.ACCOUNTANT)
|
||||
|
||||
|
||||
@register.filter
|
||||
def is_broker(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.BROKER)
|
||||
|
||||
@register.filter
|
||||
def is_installer(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.INSTALLER)
|
||||
|
||||
@register.filter
|
||||
def is_regional_water_authority(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.REGIONAL_WATER_AUTHORITY)
|
||||
|
||||
@register.filter
|
||||
def is_water_resource_manager(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.WATER_RESOURCE_MANAGER)
|
||||
|
||||
@register.filter
|
||||
def is_headquarter(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.HEADQUARTER)
|
||||
|
||||
@register.filter
|
||||
def is_customer(user):
|
||||
return _has_profile(user) and user.profile.has_role(UserRoles.CUSTOMER)
|
||||
|
3
accounts/tests.py
Normal file
3
accounts/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
13
accounts/urls.py
Normal file
13
accounts/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.urls import path
|
||||
|
||||
from accounts.views import login_view, dashboard, customer_list, add_customer_ajax, edit_customer_ajax, get_customer_data
|
||||
|
||||
app_name = "accounts"
|
||||
urlpatterns = [
|
||||
path('login/', login_view, name='login'),
|
||||
path('dashboard/', dashboard, name='dashboard'),
|
||||
path('customers/', customer_list, name='customer_list'),
|
||||
path('customers/add/', add_customer_ajax, name='add_customer_ajax'),
|
||||
path('customers/<int:customer_id>/data/', get_customer_data, name='get_customer_data'),
|
||||
path('customers/<int:customer_id>/edit/', edit_customer_ajax, name='edit_customer_ajax'),
|
||||
]
|
163
accounts/views.py
Normal file
163
accounts/views.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth import login, authenticate
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django import forms
|
||||
|
||||
from accounts.models import Profile
|
||||
from accounts.forms import CustomerForm
|
||||
from common.consts import UserRoles
|
||||
|
||||
|
||||
# Create your views here.
|
||||
def login_view(request):
|
||||
"""
|
||||
renders login page and authenticating user POST requests
|
||||
to log user in
|
||||
"""
|
||||
if request.method == "POST":
|
||||
username = request.POST.get("username")
|
||||
password = request.POST.get("password")
|
||||
user = authenticate(request, username=username, password=password)
|
||||
# if user is not None:
|
||||
# login(request, user)
|
||||
# if user.profile.has_none_of([UserRoles.MANAGER]):
|
||||
# return redirect("dashboard:dashboard")
|
||||
# else:
|
||||
# return redirect("dashboard:admin_dashboard")
|
||||
# else:
|
||||
# messages.error(request, "کاربری با این مشخصات یافت نشد!")
|
||||
# return redirect("accounts:login")
|
||||
|
||||
return render(request, "accounts/login.html")
|
||||
|
||||
def dashboard(request):
|
||||
return render(request, "accounts/dashboard.html")
|
||||
|
||||
|
||||
|
||||
def customer_list(request):
|
||||
# Get all profiles that have customer role
|
||||
customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
|
||||
|
||||
form = CustomerForm()
|
||||
return render(request, "accounts/customer_list.html", {
|
||||
"customers": customers,
|
||||
"form": form
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def add_customer_ajax(request):
|
||||
"""AJAX endpoint for adding customers"""
|
||||
form = CustomerForm(request.POST, request.FILES)
|
||||
form.request = request # Pass request to form
|
||||
if form.is_valid():
|
||||
try:
|
||||
customer = form.save()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'مشترک با موفقیت اضافه شد!',
|
||||
'customer': {
|
||||
'id': customer.id,
|
||||
'name': customer.user.get_full_name(),
|
||||
'username': customer.user.username,
|
||||
'phone': customer.phone_number_1 or 'ثبت نشده',
|
||||
'national_code': customer.national_code or 'ثبت نشده',
|
||||
'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
|
||||
}
|
||||
})
|
||||
except forms.ValidationError as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'خطا در ذخیره مشترک: {str(e)}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'خطا در اعتبارسنجی فرم',
|
||||
'errors': form.errors
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def edit_customer_ajax(request, customer_id):
|
||||
customer = get_object_or_404(Profile, id=customer_id)
|
||||
form = CustomerForm(request.POST, request.FILES, instance=customer)
|
||||
form.request = request # Pass request to form
|
||||
if form.is_valid():
|
||||
try:
|
||||
customer = form.save()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'مشترک با موفقیت ویرایش شد!',
|
||||
'customer': {
|
||||
'id': customer.id,
|
||||
'name': customer.user.get_full_name(),
|
||||
'username': customer.user.username,
|
||||
'phone': customer.phone_number_1 or 'ثبت نشده',
|
||||
'national_code': customer.national_code or 'ثبت نشده',
|
||||
'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
|
||||
}
|
||||
})
|
||||
except forms.ValidationError as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'خطا در ویرایش مشترک: {str(e)}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'خطا در اعتبارسنجی فرم',
|
||||
'errors': form.errors
|
||||
})
|
||||
|
||||
@require_GET
|
||||
def get_customer_data(request, customer_id):
|
||||
customer = get_object_or_404(Profile, id=customer_id)
|
||||
|
||||
# Create form with existing customer data
|
||||
form = CustomerForm(instance=customer, initial={
|
||||
'first_name': customer.user.first_name,
|
||||
'last_name': customer.user.last_name,
|
||||
})
|
||||
|
||||
# Render form fields as HTML
|
||||
form_html = {
|
||||
'first_name': str(form['first_name']),
|
||||
'last_name': str(form['last_name']),
|
||||
'phone_number_1': str(form['phone_number_1']),
|
||||
'phone_number_2': str(form['phone_number_2']),
|
||||
'national_code': str(form['national_code']),
|
||||
'card_number': str(form['card_number']),
|
||||
'account_number': str(form['account_number']),
|
||||
'address': str(form['address']),
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'customer': {
|
||||
'id': customer.id,
|
||||
'first_name': customer.user.first_name,
|
||||
'last_name': customer.user.last_name,
|
||||
'phone_number_1': customer.phone_number_1 or '',
|
||||
'phone_number_2': customer.phone_number_2 or '',
|
||||
'national_code': customer.national_code or '',
|
||||
'card_number': customer.card_number or '',
|
||||
'account_number': customer.account_number or '',
|
||||
'address': customer.address or ''
|
||||
},
|
||||
'form_html': form_html
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue