This commit is contained in:
Flatlogic Bot 2026-01-27 18:19:22 +00:00
parent 779ffe4f5f
commit df45ad71cc
16 changed files with 1142 additions and 191 deletions

View File

@ -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')

View 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': 'Документы',
},
),
]

View File

@ -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}"

View File

@ -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">&copy; {% 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>

View 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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View File

@ -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'),
]

View File

@ -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})

View File

@ -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;
}
}