Ver.06
BIN
assets/pasted-20260127-200020-e0bae63b.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
assets/pasted-20260127-200500-2ad7f024.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
@ -149,6 +149,9 @@ STATIC_URL = 'static/'
|
||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
BASE_DIR / 'assets',
|
||||
|
||||
@ -27,3 +27,4 @@ urlpatterns = [
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@ -1,15 +1,36 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, FleetUnit, Maintenance, Breakdown, PartRequest, Document
|
||||
from .models import Category, FleetUnit, Maintenance, Breakdown, PartRequest, Document, Supplier
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
|
||||
@admin.register(Supplier)
|
||||
class SupplierAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'representative_name', 'phone', 'email', 'contract_number')
|
||||
search_fields = ('name', 'representative_name', 'contract_number')
|
||||
|
||||
@admin.register(FleetUnit)
|
||||
class FleetUnitAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'category', 'plate_number', 'status', 'year')
|
||||
list_filter = ('status', 'category')
|
||||
search_fields = ('name', 'vin', 'plate_number')
|
||||
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status', 'commissioning_date', 'notes')
|
||||
}),
|
||||
('Страховка', {
|
||||
'fields': ('insurance_company', 'insurance_policy_number', 'insurance_start_date', 'insurance_end_date')
|
||||
}),
|
||||
('Снабжение и Поставщик', {
|
||||
'fields': ('supplier', 'vehicle_documents', 'supplier_name', 'supplier_contacts')
|
||||
}),
|
||||
('QR-код', {
|
||||
'fields': ('qr_code',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Maintenance)
|
||||
class MaintenanceAdmin(admin.ModelAdmin):
|
||||
|
||||
43
core/migrations/0004_fleetunit_insurance_company_and_more.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-27 20:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_setup_groups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='insurance_company',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Страховая компания'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='insurance_end_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата окончания страховки'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='insurance_start_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата начала страховки'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='supplier_contacts',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Контакты поставщика'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='supplier_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Поставщик / Контрагент'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='vehicle_documents',
|
||||
field=models.FileField(blank=True, null=True, upload_to='fleet_docs/', verbose_name='Документы на авто'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-27 20:11
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_fleetunit_insurance_company_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Supplier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Наименование компании')),
|
||||
('representative_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='ФИО представителя/менеджера')),
|
||||
('phone', models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Электронная почта')),
|
||||
('contract_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер договора')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Поставщик / Контрагент',
|
||||
'verbose_name_plural': 'Поставщики / Контрагенты',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='insurance_policy_number',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер страхового полиса'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fleetunit',
|
||||
name='supplier_contacts',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Контакты поставщика (старое)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fleetunit',
|
||||
name='supplier_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Поставщик (старое)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier', verbose_name='Поставщик / Контрагент'),
|
||||
),
|
||||
]
|
||||
@ -16,6 +16,20 @@ class Category(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Supplier(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Наименование компании")
|
||||
representative_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="ФИО представителя/менеджера")
|
||||
phone = models.CharField(max_length=50, blank=True, null=True, verbose_name="Телефон")
|
||||
email = models.EmailField(blank=True, null=True, verbose_name="Электронная почта")
|
||||
contract_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер договора")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Поставщик / Контрагент"
|
||||
verbose_name_plural = "Поставщики / Контрагенты"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class FleetUnit(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('active', 'В работе'),
|
||||
@ -37,6 +51,18 @@ class FleetUnit(models.Model):
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
qr_code = models.ImageField(upload_to='qrcodes/', blank=True, null=True, verbose_name="QR-код")
|
||||
|
||||
# Insurance
|
||||
insurance_company = models.CharField(max_length=255, blank=True, null=True, verbose_name="Страховая компания")
|
||||
insurance_policy_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер страхового полиса")
|
||||
insurance_start_date = models.DateField(blank=True, null=True, verbose_name="Дата начала страховки")
|
||||
insurance_end_date = models.DateField(blank=True, null=True, verbose_name="Дата окончания страховки")
|
||||
|
||||
# Supplier
|
||||
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Поставщик / Контрагент")
|
||||
supplier_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Поставщик (старое)")
|
||||
supplier_contacts = models.TextField(blank=True, null=True, verbose_name="Контакты поставщика (старое)")
|
||||
vehicle_documents = models.FileField(upload_to="fleet_docs/", blank=True, null=True, verbose_name="Документы на авто")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@ -68,13 +94,8 @@ class FleetUnit(models.Model):
|
||||
self.generate_qr_code()
|
||||
|
||||
def generate_qr_code(self):
|
||||
# We need the full URL. In a real app, we'd use the site domain.
|
||||
# For now, we'll use a relative path or a placeholder if domain is unknown.
|
||||
path = self.get_absolute_url()
|
||||
# You might want to use a full URL here if you have a domain
|
||||
# base_url = "https://yourdomain.com"
|
||||
# qr_data = f"{base_url}{path}"
|
||||
qr_data = path # Using path for now, or you can try to get site domain
|
||||
qr_data = path
|
||||
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(qr_data)
|
||||
@ -194,4 +215,4 @@ class Document(models.Model):
|
||||
verbose_name_plural = "Документы"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_doc_type_display()} - {self.uploaded_at}"
|
||||
return f"{self.get_doc_type_display()} - {self.uploaded_at}"
|
||||
@ -46,6 +46,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if 'part-request' in request.path %}active{% endif %}" href="{% url 'part_request_list' %}">Заявки</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if 'supply' in request.path or 'supplier' in request.path %}active{% endif %}" href="{% url 'supply_list' %}">Снабжение</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-white-50 me-3 d-none d-md-inline small">
|
||||
@ -81,4 +84,4 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
62
core/templates/core/breakdown_list.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-bold mb-0">Журнал поломок</h3>
|
||||
<a href="{% url 'breakdown_add' %}" class="btn btn-danger rounded-pill px-4">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>Сообщить о поломке
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="border-0 px-4">Техника</th>
|
||||
<th class="border-0">Дата</th>
|
||||
<th class="border-0">Узел / Система</th>
|
||||
<th class="border-0">Статус</th>
|
||||
<th class="border-0 text-end px-4">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in breakdowns %}
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
<div class="fw-bold"><a href="{{ b.fleet_unit.get_absolute_url }}">{{ b.fleet_unit.name }}</a></div>
|
||||
<div class="small text-muted">{{ b.fleet_unit.plate_number|default:b.fleet_unit.vin }}</div>
|
||||
</td>
|
||||
<td>{{ b.date }}</td>
|
||||
<td>{{ b.system_node }}</td>
|
||||
<td>
|
||||
{% if b.status == 'reported' %}
|
||||
<span class="badge bg-danger rounded-pill">Заявлено</span>
|
||||
{% elif b.status == 'repaired' %}
|
||||
<span class="badge bg-success rounded-pill">Отремонтировано</span>
|
||||
{% elif b.status == 'need_part' %}
|
||||
<span class="badge bg-warning text-dark rounded-pill">Нужна деталь</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary rounded-pill">{{ b.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end px-4">
|
||||
<a href="{{ b.fleet_unit.get_absolute_url }}" class="btn btn-sm btn-outline-primary rounded-pill px-3">К технике</a>
|
||||
{% if b.status == 'need_part' %}
|
||||
<a href="{% url 'part_request_add' %}?breakdown={{ b.pk }}" class="btn btn-sm btn-outline-warning rounded-pill px-3">Заказать запчасть</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">Нет зафиксированных поломок</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -40,6 +40,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if unit.insurance_company %}
|
||||
<div class="bg-light p-3 rounded mb-4">
|
||||
<h6 class="fw-bold small mb-2"><i class="bi bi-shield-check me-2"></i>Страховка</h6>
|
||||
<p class="small mb-1"><strong>Компания:</strong> {{ unit.insurance_company }}</p>
|
||||
<p class="small mb-1"><strong>Полис:</strong> {{ unit.insurance_policy_number|default:"-" }}</p>
|
||||
<p class="small mb-0"><strong>Срок:</strong> {{ unit.insurance_start_date|date:"d.m.Y" }} — {{ unit.insurance_end_date|date:"d.m.Y" }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'fleet_edit' unit.pk %}" class="btn btn-outline-primary rounded-pill">Редактировать</a>
|
||||
<a href="{% url 'breakdown_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-danger rounded-pill">Заявить о поломке</a>
|
||||
@ -73,6 +82,9 @@
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill px-4" id="parts-tab" data-bs-toggle="tab" data-bs-target="#parts" type="button">Заявки на запчасти</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill px-4" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply" type="button">Снабжение</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -169,6 +181,64 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Supply Tab -->
|
||||
<div class="tab-pane fade" id="supply">
|
||||
<h6 class="fw-bold mb-3">Данные по снабжению и поставщику</h6>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
{% if unit.supplier %}
|
||||
<p class="text-muted small mb-1">Поставщик / Контрагент</p>
|
||||
<p class="fw-bold mb-1">{{ unit.supplier.name }}</p>
|
||||
<p class="small text-muted mb-3">Договор №{{ unit.supplier.contract_number|default:"-" }}</p>
|
||||
|
||||
<p class="text-muted small mb-1">Представитель</p>
|
||||
<p class="small mb-1">{{ unit.supplier.representative_name|default:"-" }}</p>
|
||||
<p class="small mb-1"><i class="bi bi-telephone me-2"></i>{{ unit.supplier.phone|default:"-" }}</p>
|
||||
<p class="small"><i class="bi bi-envelope me-2"></i>{{ unit.supplier.email|default:"-" }}</p>
|
||||
{% else %}
|
||||
<p class="text-muted small">Поставщик не привязан</p>
|
||||
<p class="small text-muted">Вы можете выбрать поставщика в режиме редактирования техники.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<p class="text-muted small mb-1">Документы на авто</p>
|
||||
{% if unit.vehicle_documents %}
|
||||
<a href="{{ unit.vehicle_documents.url }}" class="btn btn-outline-secondary btn-sm rounded-pill" target="_blank">
|
||||
<i class="bi bi-file-earmark-text me-2"></i>Открыть документы
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="text-muted small">Не загружены</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h6 class="fw-bold mb-3">Связанные заявки на запчасти</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Деталь</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in part_requests %}
|
||||
<tr>
|
||||
<td>{{ r.part_name }}</td>
|
||||
<td><span class="badge bg-light text-dark border rounded-pill small">{{ r.get_status_display }}</span></td>
|
||||
<td class="small">{{ r.created_at|date:"d.m.Y" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="text-center py-4 text-muted">Нет заявок</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,4 +252,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,115 +1,112 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% if object %}Редактирование {{ object.name }}{% else %}Добавление техники{% endif %} | Fleet Manager
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-2">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Дашборд</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'fleet_list' %}" class="text-decoration-none">Техника</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
{% if object %}Редактирование{% else %}Добавление{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h2 class="mb-0">{% if object %}Редактирование {{ object.name }}{% else %}Новая единица техники{% endif %}</h2>
|
||||
</div>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white border-0 py-4">
|
||||
<h2 class="h4 mb-0 text-center">
|
||||
{% if object %}Редактировать технику{% else %}Добавить новую технику{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Section: Basic Info -->
|
||||
<h5 class="mb-3 text-primary border-bottom pb-2">Основная информация</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Наименование *</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Модель *</label>
|
||||
{{ form.model_name }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Категория</label>
|
||||
{{ form.category }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">VIN / Серийный номер *</label>
|
||||
{{ form.vin }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Госномер</label>
|
||||
{{ form.plate_number }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Год выпуска *</label>
|
||||
{{ form.year }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Дата ввода в эксплуатацию *</label>
|
||||
{{ form.commissioning_date }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Статус</label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label fw-medium">{{ form.name.label }}</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}<div class="text-danger small">{{ form.name.errors }}</div>{% endif %}
|
||||
<!-- Section: Insurance -->
|
||||
<h5 class="mb-3 text-primary border-bottom pb-2">Страховка</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Страховая компания</label>
|
||||
{{ form.insurance_company }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Номер полиса</label>
|
||||
{{ form.insurance_policy_number }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Дата начала</label>
|
||||
{{ form.insurance_start_date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Дата окончания</label>
|
||||
{{ form.insurance_end_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.category.id_for_label }}" class="form-label fw-medium">{{ form.category.label }}</label>
|
||||
{{ form.category }}
|
||||
{% if form.category.errors %}<div class="text-danger small">{{ form.category.errors }}</div>{% endif %}
|
||||
|
||||
<!-- Section: Supply -->
|
||||
<h5 class="mb-3 text-primary border-bottom pb-2">Снабжение</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Поставщик / Контрагент</label>
|
||||
{{ form.supplier }}
|
||||
<div class="form-text">Выберите поставщика из списка. Создать нового можно в разделе "Снабжение".</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Документы на авто (PDF/Image)</label>
|
||||
{{ form.vehicle_documents }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.model_name.id_for_label }}" class="form-label fw-medium">{{ form.model_name.label }}</label>
|
||||
{{ form.model_name }}
|
||||
{% if form.model_name.errors %}<div class="text-danger small">{{ form.model_name.errors }}</div>{% endif %}
|
||||
|
||||
<!-- Section: Media & Notes -->
|
||||
<h5 class="mb-3 text-primary border-bottom pb-2">Медиа и примечания</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Фото техники</label>
|
||||
{{ form.photo }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Примечания</label>
|
||||
{{ form.notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.vin.id_for_label }}" class="form-label fw-medium">{{ form.vin.label }}</label>
|
||||
{{ form.vin }}
|
||||
{% if form.vin.errors %}<div class="text-danger small">{{ form.vin.errors }}</div>{% endif %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-5">
|
||||
<a href="{% if object %}{% url 'fleet_detail' object.pk %}{% else %}{% url 'fleet_list' %}{% endif %}" class="btn btn-light px-4">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary px-5">Сохранить</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.plate_number.id_for_label }}" class="form-label fw-medium">{{ form.plate_number.label }}</label>
|
||||
{{ form.plate_number }}
|
||||
{% if form.plate_number.errors %}<div class="text-danger small">{{ form.plate_number.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.year.id_for_label }}" class="form-label fw-medium">{{ form.year.label }}</label>
|
||||
{{ form.year }}
|
||||
{% if form.year.errors %}<div class="text-danger small">{{ form.year.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label fw-medium">{{ form.status.label }}</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}<div class="text-danger small">{{ form.status.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.commissioning_date.id_for_label }}" class="form-label fw-medium">{{ form.commissioning_date.label }}</label>
|
||||
{{ form.commissioning_date }}
|
||||
<div class="form-text small">ГГГГ-ММ-ДД</div>
|
||||
{% if form.commissioning_date.errors %}<div class="text-danger small">{{ form.commissioning_date.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="{{ form.photo.id_for_label }}" class="form-label fw-medium">{{ form.photo.label }}</label>
|
||||
{{ form.photo }}
|
||||
{% if form.photo.errors %}<div class="text-danger small">{{ form.photo.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label fw-medium">{{ form.notes.label }}</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}<div class="text-danger small">{{ form.notes.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-5">
|
||||
<button type="submit" class="btn btn-primary px-5">
|
||||
{% if object %}Сохранить изменения{% else %}Добавить технику{% endif %}
|
||||
</button>
|
||||
<a href="{% if object %}{{ object.get_absolute_url }}{% else %}{% url 'fleet_list' %}{% endif %}" class="btn btn-outline-secondary">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add bootstrap classes to form fields
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
if (el.type !== 'checkbox' && el.type !== 'radio') {
|
||||
el.classList.add('form-control');
|
||||
}
|
||||
if (el.tagName === 'SELECT') {
|
||||
el.classList.add('form-select');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
70
core/templates/core/supplier_form.html
Normal file
@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white border-0 py-4">
|
||||
<h2 class="h4 mb-0 text-center">
|
||||
{% if object %}Редактировать поставщика{% else %}Новый поставщик{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Наименование компании *</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}<div class="text-danger small">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">ФИО представителя</label>
|
||||
{{ form.representative_name }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Телефон</label>
|
||||
{{ form.phone }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Электронная почта</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Номер договора</label>
|
||||
{{ form.contract_number }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
|
||||
<a href="{% url 'supply_list' %}" class="btn btn-light px-4">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary px-5">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
96
core/templates/core/supply_list.html
Normal file
@ -0,0 +1,96 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">Снабжение</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Part Requests Section -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Заявки на запчасти</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Дата</th>
|
||||
<th>Техника</th>
|
||||
<th>Деталь</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for req in requests %}
|
||||
<tr>
|
||||
<td>{{ req.created_at|date:"d.m.Y" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'fleet_detail' req.fleet_unit.pk %}" class="text-decoration-none">
|
||||
{{ req.fleet_unit.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ req.part_name }}</td>
|
||||
<td>{{ req.quantity }}</td>
|
||||
<td>
|
||||
<span class="badge rounded-pill bg-{{ req.status|yesno:'info,secondary' }}">
|
||||
{{ req.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">Управление</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4">Нет активных заявок</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suppliers Section -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Поставщики</h5>
|
||||
<a href="{% url 'supplier_add' %}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-lg"></i> Добавить
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for supplier in suppliers %}
|
||||
<div class="list-group-item px-0 py-3">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<h6 class="mb-1">{{ supplier.name }}</h6>
|
||||
<a href="{% url 'supplier_edit' supplier.pk %}" class="btn btn-sm btn-link p-0 text-muted">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
<p class="mb-1 small text-muted">
|
||||
<strong>Менеджер:</strong> {{ supplier.representative_name|default:"-" }}<br>
|
||||
<strong>Тел:</strong> {{ supplier.phone|default:"-" }}<br>
|
||||
<strong>Email:</strong> {{ supplier.email|default:"-" }}<br>
|
||||
<strong>Договор:</strong> {{ supplier.contract_number|default:"-" }}
|
||||
</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3 text-muted">Список поставщиков пуст</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -26,4 +26,9 @@ urlpatterns = [
|
||||
# Part Request
|
||||
path('part-request/', views.PartRequestListView.as_view(), name='part_request_list'),
|
||||
path('part-request/add/', views.PartRequestCreateView.as_view(), name='part_request_add'),
|
||||
|
||||
# Supply
|
||||
path('supply/', views.SupplyListView.as_view(), name='supply_list'),
|
||||
path('supplier/add/', views.SupplierCreateView.as_view(), name='supplier_add'),
|
||||
path('supplier/<int:pk>/edit/', views.SupplierUpdateView.as_view(), name='supplier_edit'),
|
||||
]
|
||||
110
core/views.py
@ -11,16 +11,47 @@ from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document
|
||||
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document, Supplier
|
||||
|
||||
# Forms
|
||||
class FleetUnitForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FleetUnit
|
||||
fields = ['name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status', 'commissioning_date', 'notes']
|
||||
fields = [
|
||||
'name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status',
|
||||
'commissioning_date', 'notes',
|
||||
'insurance_company', 'insurance_policy_number', 'insurance_start_date', 'insurance_end_date',
|
||||
'supplier', 'vehicle_documents'
|
||||
]
|
||||
widgets = {
|
||||
'commissioning_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||
'model_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'vin': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'plate_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'year': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'commissioning_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||||
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'insurance_policy_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'insurance_start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'insurance_end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'supplier': forms.Select(attrs={'class': 'form-select'}),
|
||||
'vehicle_documents': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class SupplierForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Supplier
|
||||
fields = ['name', 'representative_name', 'phone', 'email', 'contract_number']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'representative_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'contract_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class MaintenanceForm(forms.ModelForm):
|
||||
@ -28,8 +59,12 @@ class MaintenanceForm(forms.ModelForm):
|
||||
model = Maintenance
|
||||
fields = ['fleet_unit', 'm_type', 'planned_date', 'planned_runtime', 'mechanic', 'notes']
|
||||
widgets = {
|
||||
'planned_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||||
'm_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'planned_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'planned_runtime': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'mechanic': forms.Select(attrs={'class': 'form-select'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class BreakdownForm(forms.ModelForm):
|
||||
@ -37,16 +72,27 @@ class BreakdownForm(forms.ModelForm):
|
||||
model = Breakdown
|
||||
fields = ['fleet_unit', 'system_node', 'description', 'photo', 'status', 'notes']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||||
'system_node': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class PartRequestForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PartRequest
|
||||
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'photo', 'notes']
|
||||
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'status', 'photo', 'notes']
|
||||
widgets = {
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||||
'breakdown': forms.Select(attrs={'class': 'form-select'}),
|
||||
'part_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'article_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'quantity': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||||
}
|
||||
|
||||
# Views
|
||||
@ -194,15 +240,21 @@ class MaintenancePDFView(View):
|
||||
buffer = BytesIO()
|
||||
p = canvas.Canvas(buffer, pagesize=A4)
|
||||
|
||||
# Draw PDF content (basic for now as we don't have Cyrillic fonts loaded by default in reportlab without setup)
|
||||
# Note: For real Cyrillic support we need to register a TTF font.
|
||||
p.drawString(100, 800, f"Maintenance Act - {maintenance.m_type}")
|
||||
p.drawString(100, 780, f"Unit: {maintenance.fleet_unit.name}")
|
||||
p.drawString(100, 760, f"Date: {maintenance.actual_date}")
|
||||
p.drawString(100, 740, f"Runtime: {maintenance.actual_runtime}")
|
||||
# Register Cyrillic font
|
||||
font_path = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont('LiberationSans', font_path))
|
||||
p.setFont('LiberationSans', 12)
|
||||
except:
|
||||
pass # Fallback to default if font not found
|
||||
|
||||
p.drawString(100, 800, f"Акт технического обслуживания - {maintenance.m_type}")
|
||||
p.drawString(100, 780, f"Техника: {maintenance.fleet_unit.name}")
|
||||
p.drawString(100, 760, f"Дата: {maintenance.actual_date}")
|
||||
p.drawString(100, 740, f"Наработка: {maintenance.actual_runtime}")
|
||||
|
||||
y = 700
|
||||
p.drawString(100, y, "Checklist:")
|
||||
p.drawString(100, y, "Чек-лист:")
|
||||
y -= 20
|
||||
for item in maintenance.checklist:
|
||||
status = "[x]" if item['done'] else "[ ]"
|
||||
@ -271,4 +323,26 @@ class PartRequestCreateView(CreateView):
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
|
||||
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
|
||||
|
||||
class SupplyListView(ListView):
|
||||
model = PartRequest
|
||||
template_name = 'core/supply_list.html'
|
||||
context_object_name = 'requests'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['suppliers'] = Supplier.objects.all()
|
||||
return context
|
||||
|
||||
class SupplierCreateView(CreateView):
|
||||
model = Supplier
|
||||
form_class = SupplierForm
|
||||
template_name = 'core/supplier_form.html'
|
||||
success_url = reverse_lazy('supply_list')
|
||||
|
||||
class SupplierUpdateView(UpdateView):
|
||||
model = Supplier
|
||||
form_class = SupplierForm
|
||||
template_name = 'core/supplier_form.html'
|
||||
success_url = reverse_lazy('supply_list')
|
||||
BIN
media/qrcodes/qr-1.png
Normal file
|
After Width: | Height: | Size: 449 B |
BIN
qrcodes/qr-1.png
Normal file
|
After Width: | Height: | Size: 449 B |
@ -1,21 +1,116 @@
|
||||
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-slate-900: #1e293b;
|
||||
--text-blue-500: #3b82f6;
|
||||
--primary: #3b82f6;
|
||||
--secondary: #64748b;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--info: #06b6d4;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bg-slate-900 {
|
||||
background-color: var(--bg-slate-900) !important;
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
color: var(--text-blue-500) !important;
|
||||
}
|
||||
|
||||
.tracking-wider {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Card Styling */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
/* Stat Cards */
|
||||
.stat-card {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
padding: 0.5em 0.8em;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-gradient {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
color: white;
|
||||
padding: 3rem 0;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Form Styling */
|
||||
.form-control, .form-select {
|
||||
border-radius: 8px;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
.hero-gradient {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
}
|
||||
BIN
staticfiles/pasted-20260127-195120-f918f593.png
Normal file
|
After Width: | Height: | Size: 511 KiB |
BIN
staticfiles/pasted-20260127-200020-e0bae63b.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
staticfiles/pasted-20260127-200500-2ad7f024.png
Normal file
|
After Width: | Height: | Size: 71 KiB |