NINI Frontend Template Fayllar

NINI Frontend Template Fayllar

AdminIndex.html va TvIndex.html — to'liq tayyor, Django uchun moslangan. "Nusxa olish" tugmasini bosing.

Muhim: Bu fayllarni nini-backend/templates/ papkaga saqlang. google.script.run chaqiruvlari fetch() bilan almashtirilgan. Barcha render funksiyalar o'zgarishsiz.
AdminIndex.html
nini-backend/templates/AdminIndex.html
TvIndex.html
nini-backend/templates/TvIndex.html
NINI Franchise Django Qo'llanma
Texnik Qo'llanma

NINI Franchise
Django Backend Qo'llanma

Google Apps Script loyihangiz to'liq Django + PostgreSQL ga o'tkazildi. Quyida loyiha strukturasi, har bir faylning vazifasi, va VPS serverga 0 dan deploy qilish bo'yicha qadam-baqadam ko'rsatmalar berilgan.

13
Database modellari
Filial, Jurnal, KPI...
22
API endpointlar
Login, CRUD, Data
2
Frontend sahifalar
Admin + TV Dashboard
PostgreSQL
Database
Google Sheets o'rniga

I. Loyiha Strukturasi

Papka tuzilishi

nini-backend/
├── manage.py              ← Django boshqaruv fayli
├── requirements.txt       ← Kutubxonalar ro'yxati
├── .env                   ← Maxfiy sozlamalar (parollar)
│
├── nini_project/          ← Django project sozlamalari
│   ├── settings.py        ← Asosiy sozlamalar
│   ├── urls.py            ← Bosh URL marshrutlari
│   └── wsgi.py            ← Server uchun kirish nuqtasi
│
├── core/                  ← Asosiy ilova
│   ├── models.py          ← 13 ta database modeli
│   ├── views.py           ← 22 ta API endpoint
│   ├── urls.py            ← API URL marshrutlari
│   └── admin.py           ← Django admin paneli
│
├── static/js/
│   └── backend-calls.js   ← Frontend→Backend aloqa
│
└── templates/
    ├── AdminIndex.html    ← Admin panel sahifasi
    └── TvIndex.html       ← TV dashboard sahifasi

Nima o'zgardi?

Muhim
Eski (Google Apps Script)Yangi (Django)
Google Sheets = databasePostgreSQL database
google.script.run = APIfetch('/api/...') = REST API
Code.gs = backendviews.py = backend
CacheService = sessiyaServerda dict / Redis
Google hosting (bepul)VPS server (oyiga ~$5)

II. Database Modellari (models.py)

13 ta model

Har bir Google Sheet varag'i endi alohida database jadvali.

ModelEski Sheet nomiVazifasi
FilialFilialMarkazlar ro'yxati + arxiv holati
AdminUserAdminlarLogin/parol ma'lumotlari
TarbiyalanuvchiTarbiyalanuvchilarBolalar (o'quvchilar) ro'yxati
FoydalanuvchiFoydalanuvchilarTarbiyachilar (xodimlar)
FanFanlarGuruhga biriktirilgan fanlar
JurnalJurnalKunlik davomat + o'zlashtirish
DasturDasturO'quv reja mavzulari
TestSozlamaTest sozlamalariFan uchun ball nisbati
LavozimLavozimlarXodim lavozimlari
SozlamalarSozlamalarMuassasa nomi va rahbar
KpiSettingKPI_SettingsKPI mezonlari
KpiRecordKPI_RecordsKPI baholari
XodimDavomatXodim_DavomatXodimlar ish grafigi

III. API Endpointlar (views.py)

Barcha endpoint'lar

Har bir eski google.script.run chaqiruvi endi alohida URL.

URLMetodVazifasi
/api/login/POSTTizimga kirish
/api/check-session/POSTSessiyani tekshirish
/api/logout/POSTChiqish
/api/core-data/GETAsosiy ma'lumotlar
/api/jurnal-data/GETJurnal (lazy load)
/api/dastur-data/GETDastur (lazy load)
/api/all-data/GETHammasi birga
/api/tv-data/GETTV Dashboard uchun
/api/save-settings/POSTSozlamalar saqlash
/api/modify-fan/POSTFan CRUD
/api/modify-test/POSTTest sozlamalari CRUD
/api/modify-foyd/POSTFoydalanuvchi CRUD
/api/modify-lavozim/POSTLavozim CRUD
/api/modify-dastur/POSTDastur CRUD
/api/modify-filial/POSTFilial arxivlash/tiklash
/api/save-kpi-setting/POSTKPI sozlama saqlash
/api/save-kpi-record/POSTKPI baho yozish
/api/mark-qayta-aloqa/POSTQayta aloqa belgilash

IV. Frontend O'zgarishlar

Nima o'zgardi?

Oson

HTML va CSS to'liq saqlanadi. Faqat JavaScript'dagi backend chaqiruvlari o'zgaradi:

// ESKI (Google Apps Script):
google.script.run
  .withSuccessHandler(function(r) {
    rawData = r.data;
  })
  .getAdminCoreData();

// YANGI (Django fetch):
fetch('/api/core-data/')
  .then(r => r.json())
  .then(function(r) {
    rawData = r.data;
  });

Barcha o'zgartirilgan funksiyalar static/js/backend-calls.js faylida. AdminIndex.html ga shu faylni ulash kerak:

<script src="/static/js/backend-calls.js"></script>

Asl HTML faylingizdan quyidagi funksiyalarni o'chirib, backend-calls.js dan ishlatiladi:

  • tryRestoreSession, login, logout
  • fetchCoreData, fetchJurnalData, fetchDasturDataAsync, fetchAllData
  • setQaytaAloqa, saveSettingsFrontend
  • saveModal, delRec, saveDasturModal, deleteDasturItem
  • filialAction, saveKpiSetting, deleteKpi
  • submitKpiEval, deleteKpiEval

V. Serverga Deploy Qilish (0 dan oxirigacha)

1-qadam: VPS sotib olish

Tavsiya: DigitalOcean, Hetzner, yoki Ahost.uz dan Ubuntu 22.04 server oling.

  • Minimal: 1 CPU, 1 GB RAM, 25 GB disk ($4-6/oy)
  • IP manzil va SSH parol/kalit olasiz

2-qadam: Serverga ulanish

# Kompyuteringizdan SSH orqali ulaning
ssh root@sizning-ip-manzilingiz

3-qadam: Serverda dasturlarni o'rnatish

# 1. Tizimni yangilash
sudo apt update && sudo apt upgrade -y

# 2. Python va kerakli dasturlarni o'rnatish
sudo apt install -y python3 python3-pip python3-venv \
  postgresql postgresql-contrib nginx

# 3. PostgreSQL database yaratish
sudo -u postgres psql -c "CREATE USER nini_user
  WITH PASSWORD 'parol123';"
sudo -u postgres psql -c "CREATE DATABASE nini_db
  OWNER nini_user;"
sudo -u postgres psql -c "ALTER USER nini_user
  CREATEDB;"

4-qadam: Loyihani serverga ko'chirish

# Kompyuteringizdan fayllarni serverga yuklash:
scp -r nini-backend/ root@sizning-ip:/home/

# Yoki GitHub orqali:
cd /home
git clone https://github.com/sizning-repo/nini-backend.git

# Serverda papkaga o'ting
cd /home/nini-backend

5-qadam: Virtual muhit va kutubxonalar

# Virtual muhit yaratish
python3 -m venv venv

# Faollashtirish
source venv/bin/activate

# Kutubxonalarni o'rnatish
pip install -r requirements.txt

6-qadam: .env faylini sozlash

Muhim
# .env faylini tahrirlang:
nano .env

# Quyidagilarni yozing:
DEBUG=False
SECRET_KEY=juda-uzun-maxfiy-kalit-tasodifiy-harflar-123
DATABASE_URL=postgres://nini_user:parol123@localhost:5432/nini_db
ALLOWED_HOSTS=sizning-domain.com,sizning-ip-manzil

7-qadam: Database yaratish va admin

# Database jadvallarini yaratish
python manage.py makemigrations core
python manage.py migrate

# Statik fayllarni yig'ish
python manage.py collectstatic --noinput

# Django admin foydalanuvchisini yaratish
python manage.py createsuperuser
# (login, email, parol so'raydi)

# Birinchi admin foydalanuvchini bazaga qo'shish
python manage.py shell
# Python shellda:
from core.models import AdminUser, Filial, Sozlamalar

# Admin qo'shish
AdminUser.objects.create(
    login='admin',
    parol='admin123',
    filial='Barchasi'
)

# Filial qo'shish
Filial.objects.create(nomi='Asosiy filial', status='Faol')

# Sozlamalar
Sozlamalar.objects.create(nomi='NINI', rahbar='Direktor')

exit()

8-qadam: Gunicorn sozlash

Gunicorn — Django ni ishlab chiqarish rejimida ishga tushiruvchi server.

# Avval tekshiring — ishlayaptimi?
gunicorn nini_project.wsgi:application --bind 0.0.0.0:8000

# Brauzerda oching: http://sizning-ip:8000
# Ishlasa, Ctrl+C bilan to'xtating

# Systemd servis yaratish (avtomatik ishga tushadi):
sudo nano /etc/systemd/system/nini.service
[Unit]
Description=NINI Franchise Django
After=network.target

[Service]
User=root
Group=root
WorkingDirectory=/home/nini-backend
ExecStart=/home/nini-backend/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/home/nini-backend/nini.sock \
    nini_project.wsgi:application
Restart=always
EnvironmentFile=/home/nini-backend/.env

[Install]
WantedBy=multi-user.target
# Servisni yoqish:
sudo systemctl daemon-reload
sudo systemctl start nini
sudo systemctl enable nini
sudo systemctl status nini   # "active (running)" bo'lishi kerak

9-qadam: Nginx sozlash

Nginx — tashqi so'rovlarni qabul qilib, Gunicorn ga uzatuvchi proksi-server.

sudo nano /etc/nginx/sites-available/nini
server {
    listen 80;
    server_name sizning-domain.com sizning-ip;

    location /static/ {
        alias /home/nini-backend/staticfiles/;
    }

    location / {
        proxy_pass http://unix:/home/nini-backend/nini.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# Nginx ni yoqish:
sudo ln -s /etc/nginx/sites-available/nini \
  /etc/nginx/sites-enabled/
sudo nginx -t                # "ok" bo'lishi kerak
sudo systemctl restart nginx

Brauzerda oching: http://sizning-ip — Admin Panel ko'rinishi kerak!

10-qadam: HTTPS (SSL) o'rnatish

Bepul SSL sertifikat — Let's Encrypt orqali.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d sizning-domain.com

# Avtomatik yangilanish (har 2 oyda):
sudo certbot renew --dry-run

Endi saytingiz https://sizning-domain.com orqali ishlaydi.

VI. Kundalik Boshqaruv

Foydali buyruqlar

BuyruqNima qiladi
sudo systemctl restart niniServerni qayta ishga tushirish
sudo systemctl status niniServer holatini ko'rish
sudo journalctl -u nini -fXatolarni ko'rish (loglar)
sudo systemctl restart nginxNginx qayta ishga tushirish
python manage.py shellDatabase bilan ishlash
sizning-ip/admin/Django admin paneli (brauzerda)

Kod yangilash

Kodni o'zgartirgandan keyin:

cd /home/nini-backend
source venv/bin/activate

# Model o'zgarsa:
python manage.py makemigrations core
python manage.py migrate

# Statik fayllar o'zgarsa:
python manage.py collectstatic --noinput

# Serverni qayta ishga tushirish:
sudo systemctl restart nini

AdminIndex.html qo'yish

Muhim

Asl HTML faylingizni templates/ papkaga qo'ying. Faqat 2 ta o'zgartirish kerak:

1. </head> dan oldin qo'shing:

<script src="/static/js/backend-calls.js"></script>

2. <script> ichidagi quyidagi funksiyalarni o'chiring (ular endi backend-calls.js da):

  • tryRestoreSession, login, logout
  • fetchCoreData, fetchJurnalData, fetchDasturDataAsync, fetchAllData
  • setQaytaAloqa, saveSettingsFrontend, saveModal, delRec
  • saveDasturModal, deleteDasturItem, filialAction
  • saveKpiSetting, deleteKpi, submitKpiEval, deleteKpiEval

Qolgan barcha render funksiyalari (renderDavomat, renderAsosiyDashboard, va h.k.) o'zgarishsiz qoladi — ular faqat rawData bilan ishlaydi.

VII. Xulosa

Yaratilgan fayllar

FaylVazifasi
manage.pyDjango boshqaruv (migrate, runserver...)
requirements.txt7 ta kutubxona (Django, DRF, psycopg2...)
.envMaxfiy sozlamalar (SECRET_KEY, DATABASE_URL)
nini_project/settings.pyDatabase, CORS, middleware sozlamalari
nini_project/urls.pyAdmin sahifa + API marshrutlari
nini_project/wsgi.pyGunicorn uchun kirish nuqtasi
core/models.py13 ta database modeli
core/views.py22 ta API endpoint (login, CRUD, data)
core/urls.pyAPI URL marshrutlari
core/admin.pyDjango admin panelida jadvallar
static/js/backend-calls.jsgoogle.script.run → fetch() almashtirilgan

Keyingi qadam: VPS oling, yuqoridagi 10 ta qadamni bajaring — loyiha 30 daqiqada serverda ishlaydi.

Customize Report
NINI Backend Barcha Fayllar

NINI Franchise — Django Backend

Har bir faylni "Nusxa olish" tugmasini bosib nusxalang, keyin kompyuteringizda tegishli papkaga joylashtiring.

Papka strukturasi (avval shu papkalarni yarating)

nini-backend/
├── manage.py
├── requirements.txt
├── .env
├── nini_project/
│   ├── __init__.py          (bo'sh fayl)
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── core/
│   ├── __init__.py          (bo'sh fayl)
│   ├── models.py
│   ├── views.py
│   ├── urls.py
│   └── admin.py
├── static/
│   └── js/
│       └── backend-calls.js
└── templates/
    ├── AdminIndex.html      (asl faylingiz + o'zgartirish)
    └── TvIndex.html         (asl faylingiz + o'zgartirish)
requirements.txt
nini-backend/requirements.txt
Django>=4.2,<5.0
djangorestframework>=3.14
django-cors-headers>=4.3
psycopg2-binary>=2.9
gunicorn>=21.2
python-dotenv>=1.0
whitenoise>=6.5
.env
nini-backend/.env
DEBUG=True
SECRET_KEY=nini-franchise-maxfiy-kalit-bu-yerni-ozgartiring-2024
DATABASE_URL=postgres://nini_user:parol123@localhost:5432/nini_db
ALLOWED_HOSTS=localhost,127.0.0.1
manage.py
nini-backend/manage.py
#!/usr/bin/env python
import os
import sys

def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'nini_project.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError("Django topilmadi. pip install -r requirements.txt buyrug'ini bajaring.") from exc
    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()
settings.py
nini-backend/nini_project/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.getenv('SECRET_KEY', 'fallback-secret-key-for-dev')
DEBUG = os.getenv('DEBUG', 'True').lower() in ('true', '1', 'yes')

ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'core',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'nini_project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'nini_project.wsgi.application'

# --- DATABASE ---
db_url = os.getenv('DATABASE_URL', '')
if db_url.startswith('postgres'):
    import re
    m = re.match(r'postgres(?:ql)?://([^:]+):([^@]+)@([^:]+):(\d+)/(.+)', db_url)
    if m:
        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.postgresql',
                'USER': m.group(1),
                'PASSWORD': m.group(2),
                'HOST': m.group(3),
                'PORT': m.group(4),
                'NAME': m.group(5),
            }
        }
    else:
        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': BASE_DIR / 'db.sqlite3',
            }
        }
else:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }

AUTH_PASSWORD_VALIDATORS = []

LANGUAGE_CODE = 'uz'
TIME_ZONE = 'Asia/Tashkent'
USE_I18N = True
USE_TZ = True

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

CORS_ALLOW_ALL_ORIGINS = DEBUG
CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',
    'http://127.0.0.1:3000',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
}

SESSION_COOKIE_AGE = 6 * 60 * 60
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
urls.py (project)
nini-backend/nini_project/urls.py
from django.contrib import admin
from django.urls import path, include
from core import views

urlpatterns = [
    path('admin/', admin.site.urls),

    # Sahifalar (HTML)
    path('', views.admin_page, name='admin_page'),
    path('tv/', views.tv_page, name='tv_page'),

    # API endpointlar
    path('api/', include('core.urls')),
]
wsgi.py
nini-backend/nini_project/wsgi.py
import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'nini_project.settings')
application = get_wsgi_application()
models.py
nini-backend/core/models.py
from django.db import models

# ============================================================
#  1. FILIAL — har bir filial (markaz)
# ============================================================
class Filial(models.Model):
    STATUS_CHOICES = [('Faol', 'Faol'), ('Arxiv', 'Arxiv')]

    nomi = models.CharField(max_length=200, unique=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Faol')

    class Meta:
        verbose_name_plural = 'Filiallar'
        ordering = ['nomi']

    def __str__(self):
        return self.nomi


# ============================================================
#  2. SOZLAMALAR — muassasa nomi va rahbar (bitta qator)
# ============================================================
class Sozlamalar(models.Model):
    nomi = models.CharField(max_length=300, blank=True, default='')
    rahbar = models.CharField(max_length=300, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Sozlamalar'

    def __str__(self):
        return self.nomi or 'Sozlamalar'


# ============================================================
#  3. LAVOZIM — xodim lavozimlari
# ============================================================
class Lavozim(models.Model):
    nomi = models.CharField(max_length=200)
    filial = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Lavozimlar'

    def __str__(self):
        return self.nomi


# ============================================================
#  4. FOYDALANUVCHI — tarbiyachilar (xodimlar)
# ============================================================
class Foydalanuvchi(models.Model):
    xodim_id = models.CharField(max_length=50, unique=True)
    tarbiyachi = models.CharField(max_length=300)
    guruh = models.CharField(max_length=200)
    filial = models.CharField(max_length=200, blank=True, default='')
    lavozim = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Foydalanuvchilar'

    def __str__(self):
        return f"{self.tarbiyachi} ({self.guruh})"


# ============================================================
#  5. TARBIYALANUVCHI — bolalar (o'quvchilar)
# ============================================================
class Tarbiyalanuvchi(models.Model):
    STATUS_CHOICES = [('Faol', 'Faol'), ('Arxiv', 'Arxiv')]

    fio = models.CharField(max_length=300)
    filial = models.CharField(max_length=200, blank=True, default='')
    telefon = models.CharField(max_length=100, blank=True, default='')
    guruh = models.CharField(max_length=200)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Faol')
    ketgan_sana = models.CharField(max_length=20, blank=True, default='')
    sabab = models.CharField(max_length=500, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Tarbiyalanuvchilar'

    def __str__(self):
        return f"{self.fio} ({self.guruh})"


# ============================================================
#  6. FAN — guruhga biriktirilgan fanlar
# ============================================================
class Fan(models.Model):
    tarbiyachi = models.CharField(max_length=300)
    fan = models.CharField(max_length=200)
    guruh = models.CharField(max_length=200, blank=True, default='')
    filial = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Fanlar'

    def __str__(self):
        return f"{self.fan} — {self.tarbiyachi}"


# ============================================================
#  7. TEST SOZLAMALARI — fan uchun ball nisbati
# ============================================================
class TestSozlama(models.Model):
    fan = models.CharField(max_length=200, unique=True)
    nisbat = models.FloatField(default=1)

    class Meta:
        verbose_name_plural = 'Test sozlamalari'

    def __str__(self):
        return f"{self.fan} (x{self.nisbat})"


# ============================================================
#  8. JURNAL — kunlik davomat va o'zlashtirish
# ============================================================
class Jurnal(models.Model):
    sana = models.CharField(max_length=20)
    guruh = models.CharField(max_length=200)
    fan = models.CharField(max_length=200)
    bola_fio = models.CharField(max_length=300)
    davomat = models.CharField(max_length=50)
    ozlashtirish = models.CharField(max_length=100, blank=True, default='')
    qayta_aloqa = models.CharField(max_length=100, blank=True, default='')
    test_ball = models.FloatField(null=True, blank=True)

    class Meta:
        verbose_name_plural = 'Jurnal'
        indexes = [
            models.Index(fields=['guruh', 'sana']),
            models.Index(fields=['bola_fio', 'guruh']),
        ]

    def __str__(self):
        return f"{self.sana} | {self.bola_fio} | {self.fan}"


# ============================================================
#  9. DASTUR — o'quv rejasi (mavzular)
# ============================================================
class Dastur(models.Model):
    tarbiyachi = models.CharField(max_length=300)
    guruh = models.CharField(max_length=200)
    fan = models.CharField(max_length=200)
    mavzu = models.CharField(max_length=500)
    sana = models.CharField(max_length=20)
    filial = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Dastur'
        indexes = [
            models.Index(fields=['guruh', 'sana']),
        ]

    def __str__(self):
        return f"{self.sana} | {self.guruh} | {self.mavzu}"


# ============================================================
#  10. KPI SOZLAMALARI — KPI mezonlari
# ============================================================
class KpiSetting(models.Model):
    STATUS_CHOICES = [('Faol', 'Faol'), ('Arxiv', 'Arxiv')]

    kpi_id = models.CharField(max_length=100, unique=True)
    lavozim = models.CharField(max_length=200)
    kpi_nomi = models.CharField(max_length=300)
    maks_foiz = models.IntegerField(default=100)
    baholovchi = models.CharField(max_length=100, default='Admin')
    bosh_sana = models.CharField(max_length=20)
    tug_sana = models.CharField(max_length=20)
    variantlar = models.TextField(default='[]')
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Faol')

    class Meta:
        verbose_name_plural = 'KPI Sozlamalari'

    def __str__(self):
        return f"{self.lavozim} — {self.kpi_nomi}"


# ============================================================
#  11. KPI RECORDS — xodimlar KPI baholari
# ============================================================
class KpiRecord(models.Model):
    sana = models.CharField(max_length=30)
    xodim_id = models.CharField(max_length=50)
    guruh = models.CharField(max_length=200)
    kpi_id = models.CharField(max_length=100)
    olingan_foiz = models.IntegerField(default=0)
    izoh = models.TextField(blank=True, default='')
    admin = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'KPI Yozuvlari'

    def __str__(self):
        return f"{self.xodim_id} | {self.kpi_id} | {self.olingan_foiz}%"


# ============================================================
#  12. XODIM DAVOMAT — xodimlar ish grafigi
# ============================================================
class XodimDavomat(models.Model):
    sana = models.CharField(max_length=20)
    xodim_id = models.CharField(max_length=50)
    xodim_fio = models.CharField(max_length=300)
    guruh = models.CharField(max_length=200)
    holat = models.CharField(max_length=50)
    kelgan_vaqt = models.CharField(max_length=20, blank=True, default='')
    ketgan_vaqt = models.CharField(max_length=20, blank=True, default='')
    izoh = models.TextField(blank=True, default='')
    filial = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Xodim davomati'

    def __str__(self):
        return f"{self.sana} | {self.xodim_fio}"


# ============================================================
#  13. ADMIN — login qiluvchi adminlar
# ============================================================
class AdminUser(models.Model):
    login = models.CharField(max_length=100, unique=True)
    parol = models.CharField(max_length=100)
    filial = models.CharField(max_length=200, blank=True, default='')

    class Meta:
        verbose_name_plural = 'Adminlar'

    def __str__(self):
        return f"{self.login} ({self.filial})"
views.py
nini-backend/core/views.py
import json
import uuid
from datetime import datetime

from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST, require_GET

from .models import (
    Filial, Sozlamalar, Lavozim, Foydalanuvchi, Tarbiyalanuvchi,
    Fan, TestSozlama, Jurnal, Dastur, KpiSetting, KpiRecord,
    XodimDavomat, AdminUser,
)

# =====================================================
#  SESSION XOTIRASI (oddiy dict, productionda Redis ishlating)
# =====================================================
_sessions = {}


def _get_body(request):
    try:
        return json.loads(request.body)
    except (json.JSONDecodeError, ValueError):
        return {}


def _now_str():
    return datetime.now().strftime('%d.%m.%Y %H:%M')


def _today_str():
    return datetime.now().strftime('%d.%m.%Y')


# =====================================================
#  SAHIFALAR (HTML template'lar)
# =====================================================
def admin_page(request):
    return render(request, 'AdminIndex.html')


def tv_page(request):
    return render(request, 'TvIndex.html')


# =====================================================
#  AUTH — Login / Session / Logout
# =====================================================
@csrf_exempt
@require_POST
def api_login(request):
    data = _get_body(request)
    login = data.get('login', '').strip()
    password = data.get('password', '').strip()

    if not login or not password:
        return JsonResponse({'success': False, 'message': 'Login va parolni kiriting'})

    try:
        admin = AdminUser.objects.get(login=login, parol=password)
    except AdminUser.DoesNotExist:
        return JsonResponse({'success': False, 'message': 'Login yoki parol xato!'})

    token = str(uuid.uuid4())
    _sessions[token] = login

    return JsonResponse({
        'success': True,
        'filial': admin.filial,
        'sessionToken': token,
    })


@csrf_exempt
@require_POST
def api_check_session(request):
    data = _get_body(request)
    token = data.get('token', '')

    if not token or token not in _sessions:
        return JsonResponse({'valid': False})

    login = _sessions[token]
    try:
        admin = AdminUser.objects.get(login=login)
        return JsonResponse({
            'valid': True,
            'filial': admin.filial,
            'login': login,
        })
    except AdminUser.DoesNotExist:
        return JsonResponse({'valid': False})


@csrf_exempt
@require_POST
def api_logout(request):
    data = _get_body(request)
    token = data.get('token', '')
    _sessions.pop(token, None)
    return JsonResponse({'success': True})


# =====================================================
#  MA'LUMOT OLISH — Core Data (lazy loading)
# =====================================================
@require_GET
def api_core_data(request):
    try:
        filiallar = list(Filial.objects.values('nomi', 'status'))
        fanlar = list(Fan.objects.values('tarbiyachi', 'fan', 'guruh', 'filial'))
        test_sozlamalari = list(TestSozlama.objects.values('fan', 'nisbat'))

        foydalanuvchilar = []
        for f in Foydalanuvchi.objects.all():
            foydalanuvchilar.append({
                'id': f.xodim_id, 'tarbiyachi': f.tarbiyachi,
                'guruh': f.guruh, 'filial': f.filial, 'lavozim': f.lavozim,
            })

        bolalar = []
        for b in Tarbiyalanuvchi.objects.all():
            bolalar.append({
                'fio': b.fio, 'filial': b.filial, 'telefon': b.telefon,
                'guruh': b.guruh, 'status': b.status,
                'ketganSana': b.ketgan_sana, 'sabab': b.sabab,
            })

        soz = Sozlamalar.objects.first()
        sozlamalar = {'nomi': soz.nomi if soz else '', 'rahbar': soz.rahbar if soz else ''}
        lavozimlar = list(Lavozim.objects.values('nomi', 'filial'))

        kpi_settings = []
        for k in KpiSetting.objects.all():
            kpi_settings.append({
                'id': k.kpi_id, 'lavozim': k.lavozim, 'kpi_nomi': k.kpi_nomi,
                'maks_foiz': k.maks_foiz, 'baholovchi': k.baholovchi,
                'bosh_sana': k.bosh_sana, 'tug_sana': k.tug_sana,
                'variantlar': k.variantlar, 'status': k.status,
            })

        kpi_records = []
        for r in KpiRecord.objects.all():
            kpi_records.append({
                'sana': r.sana, 'xodim_id': r.xodim_id, 'guruh': r.guruh,
                'kpi_id': r.kpi_id, 'olingan_foiz': r.olingan_foiz,
                'izoh': r.izoh, 'admin': r.admin,
            })

        return JsonResponse({
            'success': True,
            'data': {
                'filiallar': filiallar, 'fanlar': fanlar,
                'testSozlamalari': test_sozlamalari,
                'foydalanuvchilar': foydalanuvchilar, 'bolalar': bolalar,
                'sozlamalar': sozlamalar, 'lavozimlar': lavozimlar,
                'kpiSettings': kpi_settings, 'kpiRecords': kpi_records,
                'jurnal': [], 'dastur': [],
            }
        })
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})


@require_GET
def api_jurnal_data(request):
    try:
        jurnal = []
        for j in Jurnal.objects.all():
            jurnal.append({
                'sana': j.sana, 'guruh': j.guruh, 'fan': j.fan,
                'bolaFio': j.bola_fio, 'davomat': j.davomat,
                'ozlashtirish': j.ozlashtirish, 'qaytaAloqa': j.qayta_aloqa,
                'testBall': j.test_ball,
            })
        return JsonResponse({'success': True, 'jurnal': jurnal})
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})


@require_GET
def api_dastur_data(request):
    try:
        dastur = []
        for d in Dastur.objects.all():
            dastur.append({
                'tarbiyachi': d.tarbiyachi, 'guruh': d.guruh,
                'fan': d.fan, 'mavzu': d.mavzu, 'sana': d.sana, 'filial': d.filial,
            })
        return JsonResponse({'success': True, 'dastur': dastur})
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})


@require_GET
def api_all_data(request):
    try:
        core = api_core_data(request)
        core_data = json.loads(core.content)
        if not core_data.get('success'):
            return JsonResponse(core_data)
        result = core_data['data']

        for j in Jurnal.objects.all():
            result['jurnal'].append({
                'sana': j.sana, 'guruh': j.guruh, 'fan': j.fan,
                'bolaFio': j.bola_fio, 'davomat': j.davomat,
                'ozlashtirish': j.ozlashtirish, 'qaytaAloqa': j.qayta_aloqa,
                'testBall': j.test_ball,
            })
        for d in Dastur.objects.all():
            result['dastur'].append({
                'tarbiyachi': d.tarbiyachi, 'guruh': d.guruh,
                'fan': d.fan, 'mavzu': d.mavzu, 'sana': d.sana, 'filial': d.filial,
            })

        return JsonResponse({'success': True, 'data': result})
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})


@require_GET
def api_tv_data(request):
    try:
        filiallar = list(Filial.objects.filter(status='Faol').values_list('nomi', flat=True))
        bolalar = [{'fio': b.fio, 'filial': b.filial, 'telefon': b.telefon, 'guruh': b.guruh, 'status': b.status, 'ketganSana': b.ketgan_sana, 'sabab': b.sabab} for b in Tarbiyalanuvchi.objects.all()]
        foydalanuvchilar = [{'id': f.xodim_id, 'tarbiyachi': f.tarbiyachi, 'guruh': f.guruh, 'filial': f.filial} for f in Foydalanuvchi.objects.all()]
        test_sozlamalari = list(TestSozlama.objects.values('fan', 'nisbat'))
        jurnal = [{'sana': j.sana, 'guruh': j.guruh, 'fan': j.fan, 'bolaFio': j.bola_fio, 'davomat': j.davomat, 'ozlashtirish': j.ozlashtirish, 'qaytaAloqa': j.qayta_aloqa, 'testBall': j.test_ball} for j in Jurnal.objects.all()]
        dastur = [{'tarbiyachi': d.tarbiyachi, 'guruh': d.guruh, 'fan': d.fan, 'mavzu': d.mavzu, 'sana': d.sana, 'filial': d.filial} for d in Dastur.objects.all()]
        soz = Sozlamalar.objects.first()
        sozlamalar = {'nomi': soz.nomi if soz else '', 'rahbar': soz.rahbar if soz else ''}

        return JsonResponse({'success': True, 'data': {
            'filiallar': filiallar, 'bolalar': bolalar,
            'foydalanuvchilar': foydalanuvchilar, 'testSozlamalari': test_sozlamalari,
            'jurnal': jurnal, 'dastur': dastur, 'sozlamalar': sozlamalar,
        }})
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})


# =====================================================
#  CRUD — Sozlamalar, Fanlar, Test, Foyd, Lavozim, Dastur, Filial
# =====================================================
@csrf_exempt
@require_POST
def api_save_settings(request):
    data = _get_body(request)
    soz, _ = Sozlamalar.objects.get_or_create(pk=1)
    soz.nomi = data.get('nomi', '')
    soz.rahbar = data.get('rahbar', '')
    soz.save()
    return JsonResponse({'success': True, 'message': 'Sozlamalar muvaffaqiyatli saqlandi!'})


@csrf_exempt
@require_POST
def api_modify_fan(request):
    data = _get_body(request)
    action, old, new = data.get('action', 'add'), data.get('oldData'), data.get('newData')
    if action == 'add' and new:
        Fan.objects.create(tarbiyachi=new['tarbiyachi'], fan=new['fan'], guruh=new.get('guruh', ''), filial=new.get('filial', ''))
        return JsonResponse({'message': "Fan qo'shildi!"})
    if action == 'edit' and old and new:
        f = Fan.objects.filter(tarbiyachi=old['tarbiyachi'], fan=old['fan'], guruh=old.get('guruh', '')).first()
        if f:
            f.tarbiyachi, f.fan, f.guruh, f.filial = new['tarbiyachi'], new['fan'], new.get('guruh', ''), new.get('filial', '')
            f.save()
            return JsonResponse({'message': 'Tahrirlandi!'})
        return JsonResponse({'message': 'Topilmadi!'})
    if action == 'delete' and old:
        Fan.objects.filter(tarbiyachi=old['tarbiyachi'], fan=old['fan'], guruh=old.get('guruh', '')).delete()
        return JsonResponse({'message': "O'chirildi!"})
    return JsonResponse({'message': "Noto'g'ri so'rov"})


@csrf_exempt
@require_POST
def api_modify_test(request):
    data = _get_body(request)
    action, old_fan, new = data.get('action', 'add'), data.get('oldFan'), data.get('newData')
    if action == 'add' and new:
        TestSozlama.objects.update_or_create(fan=new['fan'], defaults={'nisbat': float(new.get('nisbat', 1))})
        return JsonResponse({'message': "Test sozlamasi qo'shildi!"})
    if action == 'edit' and old_fan and new:
        try:
            t = TestSozlama.objects.get(fan=old_fan)
            t.fan, t.nisbat = new['fan'], float(new.get('nisbat', 1))
            t.save()
            return JsonResponse({'message': 'Tahrirlandi!'})
        except TestSozlama.DoesNotExist:
            return JsonResponse({'message': 'Topilmadi!'})
    if action == 'delete' and old_fan:
        TestSozlama.objects.filter(fan=old_fan).delete()
        return JsonResponse({'message': "O'chirildi!"})
    return JsonResponse({'message': "Xato so'rov"})


@csrf_exempt
@require_POST
def api_modify_foyd(request):
    data = _get_body(request)
    action, old_id, new = data.get('action', 'add'), data.get('oldId'), data.get('newData')
    if action == 'add' and new:
        Foydalanuvchi.objects.create(xodim_id=new['id'], tarbiyachi=new['tarbiyachi'], guruh=new['guruh'], filial=new.get('filial', ''), lavozim=new.get('lavozim', ''))
        return JsonResponse({'message': "Foydalanuvchi qo'shildi!"})
    if action == 'edit' and old_id and new:
        try:
            f = Foydalanuvchi.objects.get(xodim_id=old_id)
            f.xodim_id, f.tarbiyachi, f.guruh = new['id'], new['tarbiyachi'], new['guruh']
            f.filial, f.lavozim = new.get('filial', ''), new.get('lavozim', '')
            f.save()
            return JsonResponse({'message': 'Tahrirlandi!'})
        except Foydalanuvchi.DoesNotExist:
            return JsonResponse({'message': 'Topilmadi!'})
    if action == 'delete' and old_id:
        Foydalanuvchi.objects.filter(xodim_id=old_id).delete()
        return JsonResponse({'message': "O'chirildi!"})
    return JsonResponse({'message': "Xato so'rov"})


@csrf_exempt
@require_POST
def api_modify_lavozim(request):
    data = _get_body(request)
    action, new = data.get('action', 'add'), data.get('newData')
    if action == 'add' and new:
        Lavozim.objects.create(nomi=new['nomi'], filial=new.get('filial', ''))
        return JsonResponse({'message': "Lavozim qo'shildi!"})
    if action == 'delete':
        Lavozim.objects.filter(nomi=data.get('oldNomi', '')).delete()
        return JsonResponse({'message': "Lavozim o'chirildi!"})
    return JsonResponse({'message': "Xato so'rov"})


@csrf_exempt
@require_POST
def api_modify_dastur(request):
    data = _get_body(request)
    action, old, new = data.get('action', 'add'), data.get('oldData'), data.get('newData')
    if action == 'add' and new:
        Dastur.objects.create(tarbiyachi=new['tarbiyachi'], guruh=new['guruh'], fan=new['fan'], mavzu=new['mavzu'], sana=new['sana'], filial=new.get('filial', ''))
        return JsonResponse({'message': "Mavzu qo'shildi!"})
    if action == 'edit' and old and new:
        d = Dastur.objects.filter(tarbiyachi=old['tarbiyachi'], guruh=old['guruh'], fan=old['fan'], mavzu=old['mavzu'], sana=old['sana']).first()
        if d:
            d.tarbiyachi, d.guruh, d.fan = new['tarbiyachi'], new['guruh'], new['fan']
            d.mavzu, d.sana, d.filial = new['mavzu'], new['sana'], new.get('filial', '')
            d.save()
            return JsonResponse({'message': 'Tahrirlandi!'})
        return JsonResponse({'message': 'Topilmadi!'})
    if action == 'delete' and old:
        Dastur.objects.filter(tarbiyachi=old['tarbiyachi'], guruh=old['guruh'], fan=old['fan'], mavzu=old['mavzu'], sana=old['sana']).delete()
        return JsonResponse({'message': "O'chirildi!"})
    return JsonResponse({'message': "Xato so'rov"})


@csrf_exempt
@require_POST
def api_modify_filial(request):
    data = _get_body(request)
    action, filial_name = data.get('action'), data.get('filialName')
    if not filial_name:
        return JsonResponse({'success': False, 'message': 'Filial nomi kerak'})
    try:
        fil = Filial.objects.get(nomi=filial_name)
    except Filial.DoesNotExist:
        return JsonResponse({'success': False, 'message': 'Filial topilmadi'})
    if action == 'arxiv':
        fil.status = 'Arxiv'; fil.save()
        return JsonResponse({'success': True, 'message': f'{filial_name} arxivlandi!'})
    elif action == 'restore':
        fil.status = 'Faol'; fil.save()
        return JsonResponse({'success': True, 'message': f'{filial_name} qayta tiklandi!'})
    return JsonResponse({'success': False, 'message': "Noto'g'ri harakat"})


# =====================================================
#  KPI
# =====================================================
@csrf_exempt
@require_POST
def api_save_kpi_setting(request):
    data = _get_body(request)
    action = data.get('action')
    if action == 'add':
        kpi_id = f"KPI_{int(datetime.now().timestamp() * 1000)}"
        KpiSetting.objects.create(kpi_id=kpi_id, lavozim=data.get('lavozim', ''), kpi_nomi=data.get('kpi_nomi', ''), maks_foiz=int(data.get('maks_foiz', 0)), baholovchi=data.get('baholovchi', 'Admin'), bosh_sana=data.get('bosh_sana', ''), tug_sana=data.get('tug_sana', ''), variantlar=data.get('variantlar', '[]'), status='Faol')
        return JsonResponse({'success': True, 'message': 'KPI muvaffaqiyatli saqlandi!'})
    if action == 'delete':
        KpiSetting.objects.filter(kpi_id=data.get('id', '')).delete()
        return JsonResponse({'success': True, 'message': "KPI o'chirildi!"})
    return JsonResponse({'success': False, 'message': 'Harakat aniqlanmadi'})


@csrf_exempt
@require_POST
def api_save_kpi_record(request):
    data = _get_body(request)
    action = data.get('action')
    xodim_id, kpi_id = str(data.get('xodim_id', '')), str(data.get('kpi_id', ''))
    if action == 'delete':
        KpiRecord.objects.filter(xodim_id=xodim_id, kpi_id=kpi_id).delete()
        return JsonResponse({'success': True, 'message': "Baho o'chirib tashlandi!"})
    record, created = KpiRecord.objects.update_or_create(xodim_id=xodim_id, kpi_id=kpi_id, defaults={
        'sana': data.get('sana_full', _today_str()), 'guruh': data.get('guruh', ''),
        'olingan_foiz': int(data.get('olingan_foiz', 0)), 'izoh': data.get('izoh', ''),
        'admin': f"{data.get('admin', '')} ({_now_str()})",
    })
    return JsonResponse({'success': True, 'message': 'Baholash yangilandi!' if not created else 'Muvaffaqiyatli baholandi!'})


@csrf_exempt
@require_POST
def api_mark_qayta_aloqa(request):
    data = _get_body(request)
    ts = _now_str()
    record = Jurnal.objects.filter(sana=data.get('sana', ''), guruh=data.get('guruh', ''), fan=data.get('fan', ''), bola_fio=data.get('bolaFio', '')).order_by('-id').first()
    if record:
        record.qayta_aloqa = ts; record.save()
        return JsonResponse({'success': True, 'vaqt': ts})
    return JsonResponse({'success': False, 'message': "Yo'qlama topilmadi!"})


@csrf_exempt
@require_POST
def api_save_xodim_davomat(request):
    data = _get_body(request)
    record, created = XodimDavomat.objects.update_or_create(sana=data.get('sana', ''), xodim_id=data.get('xodim_id', ''), defaults={
        'xodim_fio': data.get('xodim_fio', ''), 'guruh': data.get('guruh', ''),
        'holat': data.get('holat', ''), 'kelgan_vaqt': data.get('kelgan_vaqt', ''),
        'ketgan_vaqt': data.get('ketgan_vaqt', ''), 'izoh': data.get('izoh', ''), 'filial': data.get('filial', ''),
    })
    return JsonResponse({'success': True, 'message': 'Yangilandi!' if not created else 'Saqlandi!'})


@require_GET
def api_xodim_davomat_data(request):
    try:
        result = [{'sana': x.sana, 'xodim_id': x.xodim_id, 'xodim_fio': x.xodim_fio, 'guruh': x.guruh, 'holat': x.holat, 'kelgan_vaqt': x.kelgan_vaqt, 'ketgan_vaqt': x.ketgan_vaqt, 'izoh': x.izoh, 'filial': x.filial} for x in XodimDavomat.objects.all()]
        return JsonResponse({'success': True, 'data': result})
    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)})
urls.py (core)
nini-backend/core/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('login/', views.api_login, name='api_login'),
    path('check-session/', views.api_check_session, name='api_check_session'),
    path('logout/', views.api_logout, name='api_logout'),
    path('core-data/', views.api_core_data, name='api_core_data'),
    path('jurnal-data/', views.api_jurnal_data, name='api_jurnal_data'),
    path('dastur-data/', views.api_dastur_data, name='api_dastur_data'),
    path('all-data/', views.api_all_data, name='api_all_data'),
    path('tv-data/', views.api_tv_data, name='api_tv_data'),
    path('save-settings/', views.api_save_settings, name='api_save_settings'),
    path('modify-fan/', views.api_modify_fan, name='api_modify_fan'),
    path('modify-test/', views.api_modify_test, name='api_modify_test'),
    path('modify-foyd/', views.api_modify_foyd, name='api_modify_foyd'),
    path('modify-lavozim/', views.api_modify_lavozim, name='api_modify_lavozim'),
    path('modify-dastur/', views.api_modify_dastur, name='api_modify_dastur'),
    path('modify-filial/', views.api_modify_filial, name='api_modify_filial'),
    path('save-kpi-setting/', views.api_save_kpi_setting, name='api_save_kpi_setting'),
    path('save-kpi-record/', views.api_save_kpi_record, name='api_save_kpi_record'),
    path('mark-qayta-aloqa/', views.api_mark_qayta_aloqa, name='api_mark_qayta_aloqa'),
    path('save-xodim-davomat/', views.api_save_xodim_davomat, name='api_save_xodim_davomat'),
    path('xodim-davomat-data/', views.api_xodim_davomat_data, name='api_xodim_davomat_data'),
]
admin.py
nini-backend/core/admin.py
from django.contrib import admin
from .models import (
    Filial, Sozlamalar, Lavozim, Foydalanuvchi, Tarbiyalanuvchi,
    Fan, TestSozlama, Jurnal, Dastur, KpiSetting, KpiRecord,
    XodimDavomat, AdminUser,
)

@admin.register(Filial)
class FilialAdmin(admin.ModelAdmin):
    list_display = ('nomi', 'status')
    list_filter = ('status',)

@admin.register(Sozlamalar)
class SozlamalarAdmin(admin.ModelAdmin):
    list_display = ('nomi', 'rahbar')

@admin.register(Lavozim)
class LavozimAdmin(admin.ModelAdmin):
    list_display = ('nomi', 'filial')

@admin.register(Foydalanuvchi)
class FoydalanuvchiAdmin(admin.ModelAdmin):
    list_display = ('xodim_id', 'tarbiyachi', 'guruh', 'filial', 'lavozim')
    list_filter = ('filial', 'guruh', 'lavozim')
    search_fields = ('tarbiyachi', 'xodim_id')

@admin.register(Tarbiyalanuvchi)
class TarbiyalanuvchiAdmin(admin.ModelAdmin):
    list_display = ('fio', 'guruh', 'filial', 'status', 'telefon')
    list_filter = ('status', 'filial', 'guruh')
    search_fields = ('fio', 'telefon')

@admin.register(Fan)
class FanAdmin(admin.ModelAdmin):
    list_display = ('fan', 'tarbiyachi', 'guruh', 'filial')
    list_filter = ('guruh',)

@admin.register(TestSozlama)
class TestSozlamaAdmin(admin.ModelAdmin):
    list_display = ('fan', 'nisbat')

@admin.register(Jurnal)
class JurnalAdmin(admin.ModelAdmin):
    list_display = ('sana', 'guruh', 'fan', 'bola_fio', 'davomat', 'ozlashtirish')
    list_filter = ('guruh', 'fan', 'davomat')
    search_fields = ('bola_fio',)

@admin.register(Dastur)
class DasturAdmin(admin.ModelAdmin):
    list_display = ('sana', 'guruh', 'fan', 'tarbiyachi', 'mavzu')
    list_filter = ('guruh', 'fan')
    search_fields = ('mavzu',)

@admin.register(KpiSetting)
class KpiSettingAdmin(admin.ModelAdmin):
    list_display = ('kpi_id', 'lavozim', 'kpi_nomi', 'maks_foiz', 'status')
    list_filter = ('lavozim', 'status')

@admin.register(KpiRecord)
class KpiRecordAdmin(admin.ModelAdmin):
    list_display = ('sana', 'xodim_id', 'kpi_id', 'olingan_foiz')
    list_filter = ('kpi_id',)

@admin.register(XodimDavomat)
class XodimDavomatAdmin(admin.ModelAdmin):
    list_display = ('sana', 'xodim_fio', 'guruh', 'holat')
    list_filter = ('holat', 'guruh')

@admin.register(AdminUser)
class AdminUserAdmin(admin.ModelAdmin):
    list_display = ('login', 'filial')
backend-calls.js
nini-backend/static/js/backend-calls.js
/**
 * NINI FRANCHISE — Django Backend uchun JavaScript
 * google.script.run o'rniga fetch() ishlatadi.
 * AdminIndex.html dagi <script> ichiga qo'shiladi yoki alohida yuklanadi.
 */

function apiGet(url) {
    return fetch('/api/' + url).then(function(r) { return r.json(); });
}
function apiPost(url, body) {
    return fetch('/api/' + url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body)
    }).then(function(r) { return r.json(); });
}

function tryRestoreSession() {
    var token = sessionStorage.getItem('nini_session');
    if (!token) return;
    document.getElementById('loginMsg').innerText = "Sessiya tiklanmoqda...";
    apiPost('check-session/', { token: token }).then(function(res) {
        if (res.valid) {
            sessionToken = token; currentAdmin = res.login; myFilial = res.filial;
            if (!myFilial || myFilial.trim() === '' || myFilial.toLowerCase() === 'barchasi' || myFilial.toLowerCase() === 'super admin') { isSuperAdmin = true; myFilial = ''; } else { isSuperAdmin = false; }
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('mainScreen').classList.remove('hidden');
            fetchCoreData();
        } else { sessionStorage.removeItem('nini_session'); document.getElementById('loginMsg').innerText = ''; }
    });
}

function login() {
    var log = document.getElementById('adminLogin').value, pass = document.getElementById('adminPass').value;
    if (!log || !pass) return;
    document.getElementById('loginMsg').innerText = "Tizimga ulanilmoqda...";
    currentAdmin = log;
    apiPost('login/', { login: log, password: pass }).then(function(res) {
        if (res.success) {
            myFilial = res.filial; sessionToken = res.sessionToken;
            sessionStorage.setItem('nini_session', sessionToken);
            if (!myFilial || myFilial.trim() === '' || myFilial.toLowerCase() === 'barchasi' || myFilial.toLowerCase() === 'super admin') { isSuperAdmin = true; myFilial = ''; } else { isSuperAdmin = false; }
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('mainScreen').classList.remove('hidden');
            fetchCoreData();
        } else { document.getElementById('loginMsg').innerText = res.message; }
    });
}

function logout() {
    if (sessionToken) apiPost('logout/', { token: sessionToken });
    sessionStorage.removeItem('nini_session'); sessionStorage.removeItem('nini_filters'); location.reload();
}

function fetchCoreData() {
    showLoad(true);
    apiGet('core-data/').then(function(r) {
        if (!r.success) { alert("Xato: " + r.error); showLoad(false); return; }
        rawData = r.data; rawData.jurnal = rawData.jurnal || []; rawData.dastur = rawData.dastur || [];
        setupFilialSelector(); populateDropdowns(); restoreFilters(); renderCoreViews(); showLoad(false);
        fetchJurnalData(); fetchDasturDataAsync();
    }).catch(function(e) { alert("Xatolik: " + e.message); showLoad(false); });
}

function fetchJurnalData() {
    apiGet('jurnal-data/').then(function(r) { if (r.success) { rawData.jurnal = r.jurnal; jurnalLoaded = true; renderJurnalViews(); } });
}

function fetchDasturDataAsync() {
    apiGet('dastur-data/').then(function(r) { if (r.success) { rawData.dastur = r.dastur; dasturLoaded = true; renderDasturViews(); } });
}

function fetchAllData() {
    showLoad(true);
    apiGet('all-data/').then(function(r) {
        if (!r.success) { alert("Xato: " + r.error); showLoad(false); return; }
        rawData = r.data; jurnalLoaded = true; dasturLoaded = true;
        populateDropdowns(); renderAllData(); showLoad(false);
    }).catch(function(e) { alert("Xatolik: " + e.message); showLoad(false); });
}

function setQaytaAloqa(btn, sana, guruh, fan, fio) {
    btn.disabled = true; btn.innerText = "Kuting...";
    apiPost('mark-qayta-aloqa/', { sana: sana, guruh: guruh, fan: fan, bolaFio: fio }).then(function(r) {
        if (r.success) { btn.outerHTML = '<span class="status-badge bg-grey">' + r.vaqt + '</span>'; fetchAllData(); }
        else { alert(r.message); btn.disabled = false; btn.innerText = "Belgilash"; }
    });
}

function saveSettingsFrontend() {
    var n = gv('setBogchaNomi'), r = gv('setRahbar');
    var btn = document.getElementById('btnSaveSettings'), msg = document.getElementById('msgSettings');
    btn.disabled = true; btn.innerText = "Kuting...";
    apiPost('save-settings/', { nomi: n, rahbar: r }).then(function(res) {
        btn.disabled = false; btn.innerText = "Saqlash"; msg.innerText = res.message;
        msg.style.color = res.success ? "var(--success-text)" : "var(--danger-text)";
        if (res.success && rawData.sozlamalar) { rawData.sozlamalar.nomi = n; rawData.sozlamalar.rahbar = r; }
        setTimeout(function() { msg.innerText = ''; }, 3000);
    });
}

function saveModal() {
    var btn = document.getElementById('modalSaveBtn'); btn.disabled = true; btn.innerText = "Kuting...";
    if (modalConfig.type === 'test') {
        apiPost('modify-test/', { action: modalConfig.action, oldFan: modalConfig.oldData ? modalConfig.oldData.fan : null, newData: { fan: gv('mTestFan'), nisbat: gv('mTestNisbat') } }).then(function(r) { alert(r.message); fetchAllData(); closeModal(); btn.disabled = false; btn.innerText = "Saqlash"; });
    } else if (modalConfig.type === 'fan') {
        apiPost('modify-fan/', { action: modalConfig.action, oldData: modalConfig.oldData, newData: { tarbiyachi: gv('mFanTarbiyachi'), fan: gv('mFanNomi'), guruh: stateFanGuruh, filial: myFilial } }).then(function(r) { alert(r.message); fetchAllData(); closeModal(); btn.disabled = false; btn.innerText = "Saqlash"; });
    } else if (modalConfig.type === 'foyd') {
        apiPost('modify-foyd/', { action: modalConfig.action, oldId: modalConfig.oldData ? modalConfig.oldData.id : null, newData: { id: gv('mFoydId'), tarbiyachi: gv('mFoydTarbiyachi'), guruh: gv('mFoydGuruh'), filial: myFilial, lavozim: gv('mFoydLavozim') } }).then(function(r) { alert(r.message); fetchAllData(); closeModal(); btn.disabled = false; btn.innerText = "Saqlash"; });
    } else if (modalConfig.type === 'lavozim') {
        apiPost('modify-lavozim/', { action: 'add', oldNomi: null, newData: { nomi: gv('mLavNomi'), filial: myFilial } }).then(function(r) { alert(r.message); fetchAllData(); closeModal(); btn.disabled = false; btn.innerText = "Saqlash"; });
    }
}

function delRec(type, data) {
    if (!confirm("O'chirasizmi?")) return;
    var endpoint = type === 'test' ? 'modify-test/' : type === 'fan' ? 'modify-fan/' : type === 'foyd' ? 'modify-foyd/' : 'modify-lavozim/';
    var body = type === 'test' ? { action: 'delete', oldFan: data.fan } : type === 'fan' ? { action: 'delete', oldData: data } : type === 'foyd' ? { action: 'delete', oldId: data.id } : { action: 'delete', oldNomi: data.nomi };
    apiPost(endpoint, body).then(function(r) { alert(r.message); fetchAllData(); });
}

function saveDasturModal() {
    var nd = { guruh: gv('dmGuruh'), fan: gv('dmFan'), tarbiyachi: gv('dmTarbiyachi'), mavzu: gv('dmMavzu'), sana: gv('dmSana'), filial: myFilial };
    if (!nd.guruh || !nd.fan || !nd.mavzu || !nd.sana) return alert("To'ldiring!");
    var btn = document.getElementById('dasturModalSaveBtn'); btn.disabled = true; btn.innerText = "Kuting...";
    apiPost('modify-dastur/', { action: dasturModalConfig.action, oldData: dasturModalConfig.oldData, newData: nd }).then(function(r) { alert(r.message); closeDasturModal(); btn.disabled = false; btn.innerText = "Saqlash"; fetchAllData(); });
}

function deleteDasturItem(data) {
    if (!confirm("O'chirasizmi?")) return;
    apiPost('modify-dastur/', { action: 'delete', oldData: data }).then(function(r) { alert(r.message); fetchAllData(); });
}

function filialAction(action, filialName) {
    if (!confirm(action === 'arxiv' ? filialName + " ni arxivlaysizmi?" : "Tiklaysizmi?")) return;
    showLoad(true);
    apiPost('modify-filial/', { action: action, filialName: filialName }).then(function(r) { alert(r.message); fetchAllData(); });
}

function saveKpiSetting() {
    var l = gv('kAddLavozim'), n = gv('kAddNomi'), b = gv('kAddBaholovchi'), bs = gv('kAddBoshSana'), ts = gv('kAddTugSana');
    if (!n || !bs || !ts || !l) return alert("To'ldiring!");
    var vr = document.querySelectorAll('.variant-row'); if (vr.length === 0) return alert("Variant qo'shing!");
    var va = [], mf = 0;
    for (var i = 0; i < vr.length; i++) { var fv = parseInt(vr[i].querySelector('.v-foiz').value) || 0; var nv = vr[i].querySelector('.v-nom').value.trim(); if (!nv) return alert("Nom to'ldiring!"); if (fv > mf) mf = fv; va.push({ foiz: fv, nom: nv }); }
    showLoad(true); document.getElementById('kpiAddModal').classList.add('hidden');
    apiPost('save-kpi-setting/', { action: 'add', lavozim: l, kpi_nomi: n, baholovchi: b, bosh_sana: bs, tug_sana: ts, maks_foiz: mf, variantlar: JSON.stringify(va) }).then(function(r) { alert(r.message); fetchAllData(); });
}

function deleteKpi(id) {
    if (!confirm("O'chirasizmi?")) return; showLoad(true);
    apiPost('save-kpi-setting/', { action: 'delete', id: id }).then(function(r) { fetchAllData(); });
}

function submitKpiEval() {
    if (currentSelectedFoiz === null) return alert("Variantni tanlang!");
    var btn = document.getElementById('evalSaveBtn'); btn.disabled = true; btn.innerText = 'Kuting...';
    apiPost('save-kpi-record/', { sana_full: getTodayStr(), xodim_id: currentEvalData.xodim_id, guruh: currentEvalData.guruh, kpi_id: currentEvalData.kpi_id, olingan_foiz: Number(currentSelectedFoiz), izoh: gv('evalModalIzoh').trim(), admin: currentAdmin }).then(function(r) { btn.disabled = false; btn.innerText = 'Saqlash'; document.getElementById('kpiEvalModal').classList.add('hidden'); fetchAllData(); });
}

function deleteKpiEval() {
    if (!confirm("O'chirasizmi?")) return;
    var btn = document.getElementById('evalClearBtn'); btn.disabled = true; btn.innerText = 'Kuting...';
    apiPost('save-kpi-record/', { action: 'delete', xodim_id: currentEvalData.xodim_id, kpi_id: currentEvalData.kpi_id }).then(function(r) { btn.disabled = false; btn.innerText = 'Tozalash'; document.getElementById('kpiEvalModal').classList.add('hidden'); fetchAllData(); });
}