Ver.01
This commit is contained in:
parent
779ffe4f5f
commit
df45ad71cc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,31 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, FleetUnit, Maintenance, Breakdown, PartRequest, Document
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
|
||||
@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')
|
||||
|
||||
@admin.register(Maintenance)
|
||||
class MaintenanceAdmin(admin.ModelAdmin):
|
||||
list_display = ('fleet_unit', 'm_type', 'planned_date', 'status')
|
||||
list_filter = ('status', 'm_type')
|
||||
|
||||
@admin.register(Breakdown)
|
||||
class BreakdownAdmin(admin.ModelAdmin):
|
||||
list_display = ('fleet_unit', 'system_node', 'status', 'date')
|
||||
list_filter = ('status',)
|
||||
|
||||
@admin.register(PartRequest)
|
||||
class PartRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('part_name', 'fleet_unit', 'status', 'quantity')
|
||||
list_filter = ('status',)
|
||||
|
||||
@admin.register(Document)
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ('doc_type', 'fleet_unit', 'uploaded_at')
|
||||
119
core/migrations/0001_initial.py
Normal file
119
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,119 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-27 18:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Наименование')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Категория',
|
||||
'verbose_name_plural': 'Категории',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FleetUnit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Наименование')),
|
||||
('model_name', models.CharField(max_length=255, verbose_name='Модель')),
|
||||
('vin', models.CharField(max_length=100, unique=True, verbose_name='VIN / Серийный номер')),
|
||||
('plate_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Госномер')),
|
||||
('year', models.PositiveIntegerField(verbose_name='Год выпуска')),
|
||||
('photo', models.ImageField(blank=True, null=True, upload_to='fleet_photos/', verbose_name='Фото')),
|
||||
('status', models.CharField(choices=[('active', 'В работе'), ('idle', 'Простаивает'), ('broken', 'Сломана'), ('repair', 'В ремонте'), ('waiting_parts', 'Ждёт деталь')], default='active', max_length=20, verbose_name='Статус')),
|
||||
('commissioning_date', models.DateField(verbose_name='Дата ввода в эксплуатацию')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.category', verbose_name='Категория')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Техника',
|
||||
'verbose_name_plural': 'Техника',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Breakdown',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(auto_now_add=True, verbose_name='Дата')),
|
||||
('system_node', models.CharField(max_length=255, verbose_name='Узел / система')),
|
||||
('description', models.TextField(verbose_name='Описание')),
|
||||
('status', models.CharField(choices=[('reported', 'Заявлено'), ('repaired', 'Отремонтировано'), ('need_part', 'Нужна деталь')], default='reported', max_length=20, verbose_name='Статус')),
|
||||
('repair_date', models.DateField(blank=True, null=True, verbose_name='Дата ремонта')),
|
||||
('cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Стоимость')),
|
||||
('fleet_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='breakdowns', to='core.fleetunit', verbose_name='Техника')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Поломка',
|
||||
'verbose_name_plural': 'Поломки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Maintenance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('m_type', models.CharField(choices=[('TO-250', 'ТО-250'), ('TO-500', 'ТО-500'), ('seasonal', 'Сезонное'), ('individual', 'Индивидуальное')], max_length=50, verbose_name='Тип ТО')),
|
||||
('planned_date', models.DateField(verbose_name='Плановая дата')),
|
||||
('planned_runtime', models.PositiveIntegerField(verbose_name='Плановый пробег / моточасы')),
|
||||
('status', models.CharField(choices=[('planned', 'Планируется'), ('in_progress', 'В процессе'), ('completed', 'Выполнено')], default='planned', max_length=20, verbose_name='Статус')),
|
||||
('actual_date', models.DateField(blank=True, null=True, verbose_name='Фактическая дата')),
|
||||
('actual_runtime', models.PositiveIntegerField(blank=True, null=True, verbose_name='Фактический пробег / моточасы')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
|
||||
('fleet_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='maintenances', to='core.fleetunit', verbose_name='Техника')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ТО',
|
||||
'verbose_name_plural': 'ТО',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PartRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('part_name', models.CharField(max_length=255, verbose_name='Наименование детали')),
|
||||
('article_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Артикул')),
|
||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество')),
|
||||
('status', models.CharField(choices=[('draft', 'Черновик'), ('sent', 'Отправлено'), ('ordered', 'Заказано'), ('delivered', 'Доставлено')], default='draft', max_length=20, verbose_name='Статус')),
|
||||
('photo', models.ImageField(blank=True, null=True, upload_to='part_photos/', verbose_name='Фото')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Примечание')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('fleet_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='part_requests', to='core.fleetunit', verbose_name='Техника')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Заявка на запчасть',
|
||||
'verbose_name_plural': 'Заявки на запчасти',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('doc_type', models.CharField(choices=[('passport', 'Паспорт'), ('maintenance_act', 'Акт ТО'), ('photo', 'Фото'), ('invoice', 'Счет')], max_length=50, verbose_name='Тип документа')),
|
||||
('file', models.FileField(upload_to='documents/', verbose_name='Файл')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
|
||||
('breakdown', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.breakdown')),
|
||||
('fleet_unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.fleetunit')),
|
||||
('maintenance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.maintenance')),
|
||||
('part_request', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.partrequest')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Документ',
|
||||
'verbose_name_plural': 'Документы',
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
157
core/models.py
157
core/models.py
@ -1,3 +1,158 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your models here.
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="Наименование")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Категория"
|
||||
verbose_name_plural = "Категории"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class FleetUnit(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('active', 'В работе'),
|
||||
('idle', 'Простаивает'),
|
||||
('broken', 'Сломана'),
|
||||
('repair', 'В ремонте'),
|
||||
('waiting_parts', 'Ждёт деталь'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name="Наименование")
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Категория")
|
||||
model_name = models.CharField(max_length=255, verbose_name="Модель")
|
||||
vin = models.CharField(max_length=100, unique=True, verbose_name="VIN / Серийный номер")
|
||||
plate_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Госномер")
|
||||
year = models.PositiveIntegerField(verbose_name="Год выпуска")
|
||||
photo = models.ImageField(upload_to='fleet_photos/', blank=True, null=True, verbose_name="Фото")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name="Статус")
|
||||
commissioning_date = models.DateField(verbose_name="Дата ввода в эксплуатацию")
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Техника"
|
||||
verbose_name_plural = "Техника"
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.plate_number or self.vin})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('fleet_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_status_color(self):
|
||||
colors = {
|
||||
'active': 'success',
|
||||
'idle': 'secondary',
|
||||
'broken': 'danger',
|
||||
'repair': 'warning',
|
||||
'waiting_parts': 'info',
|
||||
}
|
||||
return colors.get(self.status, 'primary')
|
||||
|
||||
class Maintenance(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('TO-250', 'ТО-250'),
|
||||
('TO-500', 'ТО-500'),
|
||||
('seasonal', 'Сезонное'),
|
||||
('individual', 'Индивидуальное'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('planned', 'Планируется'),
|
||||
('in_progress', 'В процессе'),
|
||||
('completed', 'Выполнено'),
|
||||
]
|
||||
|
||||
fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='maintenances', verbose_name="Техника")
|
||||
m_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип ТО")
|
||||
planned_date = models.DateField(verbose_name="Плановая дата")
|
||||
planned_runtime = models.PositiveIntegerField(verbose_name="Плановый пробег / моточасы")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='planned', verbose_name="Статус")
|
||||
|
||||
actual_date = models.DateField(null=True, blank=True, verbose_name="Фактическая дата")
|
||||
actual_runtime = models.PositiveIntegerField(null=True, blank=True, verbose_name="Фактический пробег / моточасы")
|
||||
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "ТО"
|
||||
verbose_name_plural = "ТО"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.m_type} - {self.fleet_unit.name} ({self.planned_date})"
|
||||
|
||||
class Breakdown(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('reported', 'Заявлено'),
|
||||
('repaired', 'Отремонтировано'),
|
||||
('need_part', 'Нужна деталь'),
|
||||
]
|
||||
|
||||
fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='breakdowns', verbose_name="Техника")
|
||||
date = models.DateField(auto_now_add=True, verbose_name="Дата")
|
||||
system_node = models.CharField(max_length=255, verbose_name="Узел / система")
|
||||
description = models.TextField(verbose_name="Описание")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='reported', verbose_name="Статус")
|
||||
repair_date = models.DateField(null=True, blank=True, verbose_name="Дата ремонта")
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Стоимость")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Поломка"
|
||||
verbose_name_plural = "Поломки"
|
||||
|
||||
def __str__(self):
|
||||
return f"Поломка: {self.fleet_unit.name} - {self.system_node}"
|
||||
|
||||
class PartRequest(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Черновик'),
|
||||
('sent', 'Отправлено'),
|
||||
('ordered', 'Заказано'),
|
||||
('delivered', 'Доставлено'),
|
||||
]
|
||||
|
||||
fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='part_requests', verbose_name="Техника")
|
||||
part_name = models.CharField(max_length=255, verbose_name="Наименование детали")
|
||||
article_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул")
|
||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Количество")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="Статус")
|
||||
photo = models.ImageField(upload_to='part_photos/', blank=True, null=True, verbose_name="Фото")
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечание")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Заявка на запчасть"
|
||||
verbose_name_plural = "Заявки на запчасти"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.part_name} for {self.fleet_unit.name}"
|
||||
|
||||
class Document(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('passport', 'Паспорт'),
|
||||
('maintenance_act', 'Акт ТО'),
|
||||
('photo', 'Фото'),
|
||||
('invoice', 'Счет'),
|
||||
]
|
||||
|
||||
doc_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип документа")
|
||||
fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='documents', null=True, blank=True)
|
||||
maintenance = models.ForeignKey(Maintenance, on_delete=models.CASCADE, related_name='documents', null=True, blank=True)
|
||||
breakdown = models.ForeignKey(Breakdown, on_delete=models.CASCADE, related_name='documents', null=True, blank=True)
|
||||
part_request = models.ForeignKey(PartRequest, on_delete=models.CASCADE, related_name='documents', null=True, blank=True)
|
||||
|
||||
file = models.FileField(upload_to='documents/', verbose_name="Файл")
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата добавления")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Документ"
|
||||
verbose_name_plural = "Документы"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_doc_type_display()} - {self.uploaded_at}"
|
||||
|
||||
@ -1,25 +1,80 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Fleet Manager{% endblock %}</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-slate-900 sticky-top shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{% url 'index' %}">
|
||||
<i class="bi bi-truck-flatbed me-2"></i>
|
||||
<span class="fw-bold text-uppercase tracking-wider">Fleet<span class="text-blue-500">Manager</span></span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-collapse="navbarSupportedContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 ms-lg-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'index' %}active{% endif %}" href="{% url 'index' %}">Дашборд</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'fleet_list' %}active{% endif %}" href="{% url 'fleet_list' %}">Техника</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">Админ-панель</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-white-50 me-3 d-none d-md-inline">Иван Иванов (Админ)</span>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="d-block link-light text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=3b82f6&color=fff" alt="mdo" width="32" height="32" class="rounded-circle">
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end text-small shadow" aria-labelledby="dropdownUser1">
|
||||
<li><a class="dropdown-item" href="#">Профиль</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Выйти</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="py-4 bg-white mt-auto border-top">
|
||||
<div class="container text-center">
|
||||
<p class="text-muted mb-0">© {% now "Y" %} Fleet Manager. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 JS -->
|
||||
<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>
|
||||
186
core/templates/core/fleet_detail.html
Normal file
186
core/templates/core/fleet_detail.html
Normal file
@ -0,0 +1,186 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ unit.name }} | Fleet Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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">{{ unit.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3">
|
||||
<h2 class="mb-0">{{ unit.name }}</h2>
|
||||
<div class="d-flex gap-2 w-100 w-md-auto">
|
||||
<a href="{% url 'fleet_edit' unit.pk %}" class="btn btn-outline-secondary flex-grow-1 flex-md-grow-0">
|
||||
<i class="bi bi-pencil me-2"></i>Редактировать
|
||||
</a>
|
||||
<button class="btn btn-danger flex-grow-1 flex-md-grow-0">
|
||||
<i class="bi bi-trash me-2"></i>Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<!-- Main Info -->
|
||||
<div class="card shadow-sm mb-4 overflow-hidden">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-5">
|
||||
{% if unit.photo %}
|
||||
<img src="{{ unit.photo.url }}" class="img-fluid h-100 object-fit-cover" alt="{{ unit.name }}" style="min-height: 300px;">
|
||||
{% else %}
|
||||
<div class="bg-light h-100 d-flex align-items-center justify-content-center border-end" style="min-height: 300px;">
|
||||
<i class="bi bi-truck text-muted display-1"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<span class="badge bg-{{ unit.get_status_color }} fs-6">
|
||||
{{ unit.get_status_display }}
|
||||
</span>
|
||||
<div class="text-muted">ID: {{ unit.pk }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Категория</label>
|
||||
<span class="fw-medium">{{ unit.category|default:"Не указана" }}</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Модель</label>
|
||||
<span class="fw-medium">{{ unit.model_name }}</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Госномер</label>
|
||||
<span class="fw-medium">{{ unit.plate_number|default:"-" }}</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Год выпуска</label>
|
||||
<span class="fw-medium">{{ unit.year }}</span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="text-muted small d-block">VIN / Серийный номер</label>
|
||||
<code class="fw-bold text-dark fs-6">{{ unit.vin }}</code>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="text-muted small d-block">Ввод в эксплуатацию</label>
|
||||
<span class="fw-medium">{{ unit.commissioning_date|date:"d.m.Y" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Tabs -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white border-bottom-0 p-0">
|
||||
<ul class="nav nav-tabs nav-fill" id="unitTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active py-3" id="m-tab" data-bs-toggle="tab" data-bs-target="#m-pane" type="button" role="tab">ТО</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="b-tab" data-bs-toggle="tab" data-bs-target="#b-pane" type="button" role="tab">Поломки</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="p-tab" data-bs-toggle="tab" data-bs-target="#p-pane" type="button" role="tab">Запчасти</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="d-tab" data-bs-toggle="tab" data-bs-target="#d-pane" type="button" role="tab">Документы</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content" id="unitTabsContent">
|
||||
<div class="tab-pane fade show active" id="m-pane" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Тип</th>
|
||||
<th>Дата</th>
|
||||
<th>Пробег/мч</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in maintenances %}
|
||||
<tr>
|
||||
<td class="ps-4">{{ m.m_type }}</td>
|
||||
<td>{{ m.planned_date|date:"d.m.Y" }}</td>
|
||||
<td>{{ m.planned_runtime }}</td>
|
||||
<td class="pe-4"><span class="badge bg-secondary">{{ m.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted italic">История ТО пуста</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="b-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Журнал поломок пуст</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="p-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Заявок на запчасти нет</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="d-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Документы не загружены</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Actions & Quick Stats -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Действия</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary py-3">
|
||||
<i class="bi bi-tools me-2"></i>Зафиксировать поломку
|
||||
</button>
|
||||
<button class="btn btn-outline-primary py-3">
|
||||
<i class="bi bi-calendar-plus me-2"></i>Запланировать ТО
|
||||
</button>
|
||||
<button class="btn btn-outline-info py-3">
|
||||
<i class="bi bi-cart-plus me-2"></i>Заявка на запчасть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Placeholder -->
|
||||
<div class="card shadow-sm mb-4 text-center">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title mb-3">QR-код техники</h5>
|
||||
<div class="bg-light p-3 rounded mb-3 mx-auto" style="width: 200px; height: 200px;">
|
||||
<!-- Placeholder for QR Code generation -->
|
||||
<div class="w-100 h-100 border border-2 border-dashed d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-qr-code text-muted display-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mb-3">Отсканируйте для быстрого доступа с мобильного телефона</p>
|
||||
<button class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-download me-2"></i>Скачать QR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-2">Примечания</h5>
|
||||
<p class="text-muted small mb-0">{{ unit.notes|default:"Примечания отсутствуют" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
115
core/templates/core/fleet_form.html
Normal file
115
core/templates/core/fleet_form.html
Normal file
@ -0,0 +1,115 @@
|
||||
{% 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="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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</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 %}
|
||||
119
core/templates/core/fleet_list.html
Normal file
119
core/templates/core/fleet_list.html
Normal file
@ -0,0 +1,119 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Список техники | Fleet Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4 gap-3">
|
||||
<div>
|
||||
<h2 class="mb-1">Парк техники</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Дашборд</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Техника</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<a href="{% url 'fleet_add' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg me-2"></i>Добавить технику
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="Поиск по названию или номеру..." value="{{ request.GET.search }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="status" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>В работе</option>
|
||||
<option value="idle" {% if request.GET.status == 'idle' %}selected{% endif %}>Простаивает</option>
|
||||
<option value="broken" {% if request.GET.status == 'broken' %}selected{% endif %}>Сломана</option>
|
||||
<option value="repair" {% if request.GET.status == 'repair' %}selected{% endif %}>В ремонте</option>
|
||||
<option value="waiting_parts" {% if request.GET.status == 'waiting_parts' %}selected{% endif %}>Ждёт деталь</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-outline-secondary w-100">Применить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for unit in units %}
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="row g-0 h-100">
|
||||
<div class="col-4">
|
||||
{% if unit.photo %}
|
||||
<img src="{{ unit.photo.url }}" class="img-fluid rounded-start h-100 object-fit-cover" alt="{{ unit.name }}" style="min-height: 160px;">
|
||||
{% else %}
|
||||
<div class="bg-light rounded-start h-100 d-flex align-items-center justify-content-center border-end" style="min-height: 160px;">
|
||||
<i class="bi bi-truck text-muted display-6"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="card-body d-flex flex-column h-100">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<span class="badge bg-{{ unit.get_status_color }} bg-opacity-10 text-{{ unit.get_status_color }} border border-{{ unit.get_status_color }}">
|
||||
{{ unit.get_status_display }}
|
||||
</span>
|
||||
<span class="text-muted small">#{{ unit.pk }}</span>
|
||||
</div>
|
||||
<h5 class="card-title mb-1">
|
||||
<a href="{{ unit.get_absolute_url }}" class="text-decoration-none text-slate-900">{{ unit.name }}</a>
|
||||
</h5>
|
||||
<p class="card-text text-muted small mb-3">
|
||||
{{ unit.model_name }} | {{ unit.plate_number|default:unit.vin }}
|
||||
</p>
|
||||
<div class="mt-auto d-flex justify-content-between align-items-center">
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ unit.year }}
|
||||
</div>
|
||||
<a href="{{ unit.get_absolute_url }}" class="btn btn-sm btn-outline-primary">Подробнее</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-inbox text-muted display-1"></i>
|
||||
</div>
|
||||
<h4 class="text-muted">Техника не найдена</h4>
|
||||
<p class="text-muted">Попробуйте изменить параметры поиска или добавить новую единицу.</p>
|
||||
<a href="{% url 'fleet_add' %}" class="btn btn-primary mt-2">Добавить технику</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav class="mt-5">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Назад</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% for i in paginator.page_range %}
|
||||
<li class="page-item {% if page_obj.number == i %}active{% endif %}">
|
||||
<a class="page-link" href="?page={{ i }}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">{{ i }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Вперед</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -1,145 +1,154 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
: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);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Дашборд | Fleet Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="hero-gradient px-4 px-md-5 d-flex align-items-center">
|
||||
<div>
|
||||
<h1 class="display-5 mb-2">Управление парком техники</h1>
|
||||
<p class="lead opacity-75">Централизованная система контроля, обслуживания и учета вашей техники.</p>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<a href="{% url 'fleet_add' %}" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-plus-lg me-2"></i>Добавить технику
|
||||
</a>
|
||||
<a href="{% url 'fleet_list' %}" class="btn btn-outline-light btn-lg">
|
||||
<i class="bi bi-list-ul me-2"></i>Весь список
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-primary bg-opacity-10 text-primary">
|
||||
<i class="bi bi-truck"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Всего единиц</div>
|
||||
<div class="h3 mb-0">{{ total_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-success bg-opacity-10 text-success">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">В работе</div>
|
||||
<div class="h3 mb-0">{{ active_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-danger bg-opacity-10 text-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Сломано</div>
|
||||
<div class="h3 mb-0">{{ broken_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-warning bg-opacity-10 text-warning">
|
||||
<i class="bi bi-tools"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">В ремонте</div>
|
||||
<div class="h3 mb-0">{{ repair_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Предстоящие ТО</h5>
|
||||
<a href="#" class="btn btn-sm btn-link text-decoration-none">Все ТО</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Техника</th>
|
||||
<th>Тип</th>
|
||||
<th>Дата</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in recent_maintenances %}
|
||||
<tr>
|
||||
<td class="ps-4 fw-medium">{{ m.fleet_unit.name }}</td>
|
||||
<td>{{ m.m_type }}</td>
|
||||
<td>{{ m.planned_date|date:"d.m.Y" }}</td>
|
||||
<td class="pe-4">
|
||||
<span class="badge bg-{{ m.status|yesno:'info,warning,success' }}">
|
||||
{{ m.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted italic">Нет запланированных ТО</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Последние поломки</h5>
|
||||
<a href="#" class="btn btn-sm btn-link text-decoration-none">Журнал</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Техника</th>
|
||||
<th>Узел</th>
|
||||
<th>Дата</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in recent_breakdowns %}
|
||||
<tr>
|
||||
<td class="ps-4 fw-medium">{{ b.fleet_unit.name }}</td>
|
||||
<td>{{ b.system_node }}</td>
|
||||
<td>{{ b.date|date:"d.m.Y" }}</td>
|
||||
<td class="pe-4">
|
||||
<span class="badge bg-{{ b.status|yesno:'danger,success,warning' }}">
|
||||
{{ b.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted italic">Поломок не зафиксировано</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
11
core/urls.py
11
core/urls.py
@ -1,7 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
]
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
path('fleet/', views.FleetListView.as_view(), name='fleet_list'),
|
||||
path('fleet/<int:pk>/', views.FleetDetailView.as_view(), name='fleet_detail'),
|
||||
path('fleet/add/', views.FleetCreateView.as_view(), name='fleet_add'),
|
||||
path('fleet/<int:pk>/edit/', views.FleetUpdateView.as_view(), name='fleet_edit'),
|
||||
]
|
||||
@ -1,25 +1,75 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView
|
||||
from django.urls import reverse_lazy
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category
|
||||
|
||||
class FleetUnitForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FleetUnit
|
||||
fields = ['name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status', 'commissioning_date', 'notes']
|
||||
widgets = {
|
||||
'commissioning_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
class IndexView(TemplateView):
|
||||
template_name = 'core/index.html'
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['total_units'] = FleetUnit.objects.count()
|
||||
context['active_units'] = FleetUnit.objects.filter(status='active').count()
|
||||
context['broken_units'] = FleetUnit.objects.filter(status='broken').count()
|
||||
context['repair_units'] = FleetUnit.objects.filter(status='repair').count()
|
||||
|
||||
# Stats for charts or badges
|
||||
context['status_counts'] = FleetUnit.objects.values('status').annotate(total=Count('status'))
|
||||
|
||||
context['recent_maintenances'] = Maintenance.objects.all().order_by('-planned_date')[:5]
|
||||
context['recent_breakdowns'] = Breakdown.objects.all().order_by('-date')[:5]
|
||||
|
||||
return context
|
||||
|
||||
class FleetListView(ListView):
|
||||
model = FleetUnit
|
||||
template_name = 'core/fleet_list.html'
|
||||
context_object_name = 'units'
|
||||
paginate_by = 12
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
status = self.request.GET.get('status')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(name__icontains=search) | queryset.filter(plate_number__icontains=search) | queryset.filter(vin__icontains=search)
|
||||
return queryset
|
||||
|
||||
class FleetDetailView(DetailView):
|
||||
model = FleetUnit
|
||||
template_name = 'core/fleet_detail.html'
|
||||
context_object_name = 'unit'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['maintenances'] = self.object.maintenances.all()
|
||||
context['breakdowns'] = self.object.breakdowns.all()
|
||||
context['part_requests'] = self.object.part_requests.all()
|
||||
return context
|
||||
|
||||
class FleetCreateView(CreateView):
|
||||
model = FleetUnit
|
||||
template_name = 'core/fleet_form.html'
|
||||
form_class = FleetUnitForm
|
||||
success_url = reverse_lazy('fleet_list')
|
||||
|
||||
class FleetUpdateView(UpdateView):
|
||||
model = FleetUnit
|
||||
template_name = 'core/fleet_form.html'
|
||||
form_class = FleetUnitForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('fleet_detail', kwargs={'pk': self.object.pk})
|
||||
@ -1,4 +1,116 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
:root {
|
||||
--bg-slate-900: #1e293b;
|
||||
--text-blue-500: #3b82f6;
|
||||
--primary: #3b82f6;
|
||||
--secondary: #64748b;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--info: #06b6d4;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user