начал работать с интерфейсом
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s
This commit is contained in:
4
.env
4
.env
@@ -1,12 +1,12 @@
|
|||||||
SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms'
|
|
||||||
# Настройки базы данных
|
# Настройки базы данных
|
||||||
DB_NAME=prodman_db
|
DB_NAME=prodman_db
|
||||||
DB_USER=prodman_user
|
DB_USER=prodman_user
|
||||||
DB_PASS=prodman_password_zwE45t!
|
DB_PASS=prodman_password_zwE45t!
|
||||||
|
|
||||||
# Настройки Django
|
# Настройки Django
|
||||||
ENV_TYPE=server
|
ENV_TYPE=dev
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
|
SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms'
|
||||||
# todo потом установить домен для продакшена
|
# todo потом установить домен для продакшена
|
||||||
ALLOWED_HOSTS=192.168.1.108,shiftflow.tertelius.space
|
ALLOWED_HOSTS=192.168.1.108,shiftflow.tertelius.space
|
||||||
CSRF_ORIGINS=http://192.168.1.108,https://shiftflow.tertelius.space
|
CSRF_ORIGINS=http://192.168.1.108,https://shiftflow.tertelius.space
|
||||||
|
|||||||
@@ -16,15 +16,20 @@
|
|||||||
### 💊 Таблетка: Если не пушится (сброс авторизации)
|
### 💊 Таблетка: Если не пушится (сброс авторизации)
|
||||||
Если Git "забыл" пароль или выдает ошибку Permission Denied:
|
Если Git "забыл" пароль или выдает ошибку Permission Denied:
|
||||||
|
|
||||||
```bash
|
|
||||||
# Очищаем старые привязки
|
# Очищаем старые привязки
|
||||||
|
```bash
|
||||||
git config --global --unset credential.helper
|
git config --global --unset credential.helper
|
||||||
|
```
|
||||||
|
|
||||||
# Устанавливаем менеджер заново (для Windows)
|
# Устанавливаем менеджер заново (для Windows)
|
||||||
|
```bash
|
||||||
git config --global credential.helper manager
|
git config --global credential.helper manager
|
||||||
|
```
|
||||||
# Снова пушим и вводим логин/пароль в окне
|
# Снова пушим и вводим логин/пароль в окне
|
||||||
|
```bash
|
||||||
git push origin main
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
# 🚀 ShiftFlow MES - Инструкция по деплою (Production)
|
# 🚀 ShiftFlow MES - Инструкция по деплою (Production)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ import environ
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
env = environ.Env()
|
env = environ.Env()
|
||||||
environ.Env.read_env() # Читаем файл .env
|
# environ.Env.read_env() # Читаем файл .env
|
||||||
|
|
||||||
|
# Явно указываем путь к файлу в корне
|
||||||
|
env_file = os.path.join(BASE_DIR, ".env")
|
||||||
|
if os.path.exists(env_file):
|
||||||
|
environ.Env.read_env(env_file)
|
||||||
|
print(f"Файл .env найден и прочитан: {env_file}")
|
||||||
|
else:
|
||||||
|
print(f"ОШИБКА: Файл .env не найден по пути: {env_file}")
|
||||||
|
|
||||||
# читаем переменную окружения
|
# читаем переменную окружения
|
||||||
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
||||||
@@ -164,9 +172,16 @@ USE_TZ = True
|
|||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
|
|
||||||
MEDIA_URL = 'media/'
|
MEDIA_URL = 'media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
# Куда переходим после логина
|
||||||
|
LOGIN_REDIRECT_URL = '/' # Куда идем после входа
|
||||||
|
LOGOUT_REDIRECT_URL = '/' # Куда идем после выхода
|
||||||
|
|
||||||
# Доверяем прокси-серверу передавать заголовки
|
# Доверяем прокси-серверу передавать заголовки
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
@@ -179,7 +194,13 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
|||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = env.list('CSRF_ORIGINS', default=['http://localhost'])
|
CSRF_TRUSTED_ORIGINS = env.list('CSRF_ORIGINS', default=['http://localhost'])
|
||||||
|
|
||||||
|
|
||||||
print(f"--- РАБОТАЕМ НА БАЗЕ: {DATABASES['default']['NAME']} (HOST: {DATABASES['default'].get('HOST', 'localhost')}) ---")
|
print(f"--- РАБОТАЕМ НА БАЗЕ: {DATABASES['default']['NAME']} (HOST: {DATABASES['default'].get('HOST', 'localhost')}) ---")
|
||||||
|
|
||||||
print (env)
|
|
||||||
|
# Проверяем, видит ли он базу и режим отладки
|
||||||
|
print(f"DB_NAME: {env('DB_NAME', default='НЕ НАЙДЕНО')}")
|
||||||
|
print(f"ENV_TYPE: {env('ENV_TYPE', default='False')}")
|
||||||
|
print(f"SECRET_KEY: {env('SECRET_KEY', default='False')}")
|
||||||
|
print(f"CSRF_TRUSTED_ORIGINS: {CSRF_TRUSTED_ORIGINS}")
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,19 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.conf.urls.static import static # <--- Добавьте эту строку
|
from django.conf.urls.static import static # <--- Добавьте эту строку
|
||||||
from core import settings
|
from core import settings
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
# Добавь эту строку, она подключит login, logout и прочие стандартные пути
|
||||||
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
|
# Подключаем урлы нашего приложения
|
||||||
|
path('', include('shiftflow.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Вместо if settings.DEBUG: не забываем from django.conf.urls.static import static # <--- Добавьте эту строку
|
# Вместо if settings.DEBUG: не забываем from django.conf.urls.static import static # <--- Добавьте эту строку
|
||||||
if settings.ENV_TYPE in ['local', 'dev']:
|
if settings.ENV_TYPE in ['local', 'dev']:
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Company, Machine, Deal, Material, Item
|
from .models import Company, EmployeeProfile, Machine, Deal, Material, Item
|
||||||
|
|
||||||
# --- Настройка отображения Компаний ---
|
# --- Настройка отображения Компаний ---
|
||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
@@ -11,7 +11,7 @@ class CompanyAdmin(admin.ModelAdmin):
|
|||||||
# --- Настройка отображения Сделок ---
|
# --- Настройка отображения Сделок ---
|
||||||
@admin.register(Deal)
|
@admin.register(Deal)
|
||||||
class DealAdmin(admin.ModelAdmin):
|
class DealAdmin(admin.ModelAdmin):
|
||||||
list_display = ('number', 'company', 'created_at')
|
list_display = ('number', 'company')
|
||||||
search_fields = ('number', 'company__name')
|
search_fields = ('number', 'company__name')
|
||||||
list_filter = ('company',)
|
list_filter = ('company',)
|
||||||
|
|
||||||
@@ -69,3 +69,8 @@ class ItemAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
# Регистрация станков просто списком
|
# Регистрация станков просто списком
|
||||||
admin.site.register(Machine)
|
admin.site.register(Machine)
|
||||||
|
|
||||||
|
@admin.register(EmployeeProfile)
|
||||||
|
class EmployeeProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'role')
|
||||||
|
filter_horizontal = ('machines',) # Красивый выбор станков двумя колонками
|
||||||
29
shiftflow/migrations/0003_employeeprofile.py
Normal file
29
shiftflow/migrations/0003_employeeprofile.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-28 15:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shiftflow', '0002_company_material_alter_item_options_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EmployeeProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.CharField(choices=[('admin', 'Администратор'), ('technologist', 'Технолог'), ('master', 'Мастер'), ('operator', 'Оператор'), ('clerk', 'Учетчик')], default='operator', max_length=20, verbose_name='Должность')),
|
||||||
|
('machines', models.ManyToManyField(blank=True, to='shiftflow.machine', verbose_name='Закрепленные станки')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Профиль сотрудника',
|
||||||
|
'verbose_name_plural': 'Профили сотрудников',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -95,3 +95,25 @@ class Item(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.drawing_name} ({self.quantity_plan} шт.)"
|
return f"{self.drawing_name} ({self.quantity_plan} шт.)"
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeeProfile(models.Model):
|
||||||
|
ROLE_CHOICES = [
|
||||||
|
('admin', 'Администратор'),
|
||||||
|
('technologist', 'Технолог'),
|
||||||
|
('master', 'Мастер'),
|
||||||
|
('operator', 'Оператор'),
|
||||||
|
('clerk', 'Учетчик'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Связь 1 к 1 со стандартным юзером Django
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name='Пользователь')
|
||||||
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='operator', verbose_name='Должность')
|
||||||
|
# Привязка станков (можно выбрать несколько для одного оператора)
|
||||||
|
machines = models.ManyToManyField('Machine', blank=True, verbose_name='Закрепленные станки')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} - {self.get_role_display()}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Профиль сотрудника'
|
||||||
|
verbose_name_plural = 'Профили сотрудников'
|
||||||
65
shiftflow/templates/shiftflow/items_list copy.html
Normal file
65
shiftflow/templates/shiftflow/items_list copy.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'shiftflow/partials/_filter.html' %}
|
||||||
|
|
||||||
|
{% if not selected_machines %}
|
||||||
|
<div class="alert alert-warning text-center mt-4">
|
||||||
|
<i class="bi bi-info-circle me-2"></i> Станки не выбраны. Выберите станки в фильтре для отображения позиций.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% if in_work is not None %}
|
||||||
|
<h6 class="text-primary mt-4 fw-bold"><i class="bi bi-play-circle me-2"></i>В РАБОТЕ</h6>
|
||||||
|
<div class="table-responsive rounded border shadow-sm mb-4">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-custom-header small fw-bold text-uppercase">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;">Сделка</th>
|
||||||
|
<th>Деталь / Чертеж</th>
|
||||||
|
<th>Материал</th>
|
||||||
|
<th class="text-center">План</th>
|
||||||
|
<th class="text-center">Факт</th>
|
||||||
|
{% if user_role in 'admin,technologist,master,operator' %}
|
||||||
|
<th class="text-end">Действия</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in in_work %}
|
||||||
|
<tr>
|
||||||
|
<td class="small">{{ item.deal.number }}</td>
|
||||||
|
<td><strong>{{ item.drawing_name }}</strong> <small class="text-muted">({{ item.machine.name }})</small></td>
|
||||||
|
<td><span class="badge bg-secondary opacity-75">{{ item.material.name }}</span></td>
|
||||||
|
<td class="text-center fw-bold">{{ item.quantity_plan }}</td>
|
||||||
|
<td class="text-center" style="width: 100px;">
|
||||||
|
<input type="number" class="form-control form-control-sm text-center" value="{{ item.quantity_fact|default:0 }}" {% if user_role == 'clerk' %}readonly{% endif %}>
|
||||||
|
</td>
|
||||||
|
{% if user_role in 'admin,technologist,master,operator' %}
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-success"><i class="bi bi-check2-all"></i></button>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6" class="text-center py-4 text-muted small">Активных заданий нет</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if backlog is not None %}
|
||||||
|
<h6 class="text-danger fw-bold"><i class="bi bi-exclamation-triangle me-2"></i>НЕДОДЕЛЫ (ХВОСТЫ)</h6>
|
||||||
|
<div class="table-responsive rounded border shadow-sm mb-4">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if done_items is not None %}
|
||||||
|
<h6 class="text-success fw-bold"><i class="bi bi-check-circle me-2"></i>ЗАВЕРШЕНО</h6>
|
||||||
|
<div class="table-responsive rounded border shadow-sm">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
38
shiftflow/templates/shiftflow/items_list.html
Normal file
38
shiftflow/templates/shiftflow/items_list.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card shadow bg-dark text-light border-secondary">
|
||||||
|
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="text-accent mb-0">Реестр деталей</h3>
|
||||||
|
{% if user_role == 'admin' or user_role == 'technologist' %}
|
||||||
|
<button class="btn btn-sm btn-outline-accent">+ Добавить заказ</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-dark table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th>Кол-во</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.id }}</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ item.status }}</span></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-muted">Деталей пока нет</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
12
shiftflow/templates/shiftflow/landing.html
Normal file
12
shiftflow/templates/shiftflow/landing.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-accent mb-4 display-3 fw-bold">
|
||||||
|
<i class="bi bi-gear-fill me-3"></i>ShiftFlow
|
||||||
|
</h1>
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-lg btn-outline-accent px-5 py-3 fw-bold shadow">
|
||||||
|
ВОЙТИ В СИСТЕМУ
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
45
shiftflow/templates/shiftflow/partials/_filter.html
Normal file
45
shiftflow/templates/shiftflow/partials/_filter.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="card border-secondary mb-3 shadow-sm">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<form method="get" id="filter-form" class="row g-2 align-items-center">
|
||||||
|
<input type="hidden" name="filtered" value="1"> <div class="col-md-4">
|
||||||
|
<div class="small text-muted mb-1 fw-bold">Станки:</div>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
{% for m in machines %}
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="btn-check" name="m_ids" id="m_{{ m.id }}" value="{{ m.id }}"
|
||||||
|
{% if m.id in selected_machines %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="small text-muted mb-1 fw-bold">Статус:</div>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
<input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="btn-check" name="statuses" id="s_partial" value="partial" {% if 'partial' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-danger btn-sm" for="s_partial">Недодел</label>
|
||||||
|
|
||||||
|
<input type="checkbox" class="btn-check" name="statuses" id="s_done" value="done" {% if 'done' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
|
||||||
|
<label class="btn btn-outline-success btn-sm" for="s_done">Завершено</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="small text-muted mb-1 fw-bold">С:</label>
|
||||||
|
<input type="date" name="start_date" class="form-control form-control-sm" value="{{ start_date }}" onchange="this.form.submit()">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="small text-muted mb-1 fw-bold">По:</label>
|
||||||
|
<input type="date" name="end_date" class="form-control form-control-sm" value="{{ end_date }}" onchange="this.form.submit()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-1 text-end mt-auto">
|
||||||
|
<a href="{% url 'registry' %}" class="btn btn-outline-secondary btn-sm w-100" title="Сбросить по умолчанию"><i class="bi bi-x-circle"></i></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
10
shiftflow/urls.py
Normal file
10
shiftflow/urls.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import IndexView, RegistryView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Главная страница (путь пустой)
|
||||||
|
path('', IndexView.as_view(), name='index'),
|
||||||
|
|
||||||
|
# Реестр
|
||||||
|
path('registry/', RegistryView.as_view(), name='registry'),
|
||||||
|
]
|
||||||
72
shiftflow/views copy.py
Normal file
72
shiftflow/views copy.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from .models import Item, Machine
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def items_list_view(request):
|
||||||
|
# Если не авторизован, просто отдаем пустой контекст для страницы входа
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return render(request, 'shiftflow/items_list.html', {})
|
||||||
|
|
||||||
|
# ОПРЕДЕЛЕНИЕ РОЛИ (Ищем в группах Джанго или ставим суперюзера)
|
||||||
|
user_role = 'guest'
|
||||||
|
if request.user.is_superuser:
|
||||||
|
user_role = 'admin'
|
||||||
|
elif request.user.groups.filter(name='Технолог').exists():
|
||||||
|
user_role = 'technologist'
|
||||||
|
elif request.user.groups.filter(name='Мастер').exists():
|
||||||
|
user_role = 'master'
|
||||||
|
elif request.user.groups.filter(name='Оператор').exists():
|
||||||
|
user_role = 'operator'
|
||||||
|
elif request.user.groups.filter(name='Учетчик').exists():
|
||||||
|
user_role = 'clerk'
|
||||||
|
|
||||||
|
# ФИЛЬТРЫ
|
||||||
|
all_machines = Machine.objects.all()
|
||||||
|
all_machine_ids = list(all_machines.values_list('id', flat=True))
|
||||||
|
|
||||||
|
# Проверяем, был ли нажат фильтр (есть ли параметр 'filtered' в URL)
|
||||||
|
is_filtered = 'filtered' in request.GET
|
||||||
|
|
||||||
|
if is_filtered:
|
||||||
|
selected_machines = [int(x) for x in request.GET.getlist('m_ids')]
|
||||||
|
selected_statuses = request.GET.getlist('statuses')
|
||||||
|
start_date = request.GET.get('start_date')
|
||||||
|
end_date = request.GET.get('end_date')
|
||||||
|
else:
|
||||||
|
# Значения по умолчанию (при первом заходе или сбросе)
|
||||||
|
selected_machines = all_machine_ids
|
||||||
|
selected_statuses = ['work', 'partial'] # В работе и недоделы
|
||||||
|
start_date = timezone.now().strftime('%Y-%m-%d')
|
||||||
|
end_date = timezone.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# ВЫБОРКА ДАННЫХ
|
||||||
|
items = Item.objects.select_related('deal', 'material', 'machine')
|
||||||
|
|
||||||
|
# Защита логики: если станки не выбраны — список пуст
|
||||||
|
if not selected_machines:
|
||||||
|
items = Item.objects.none()
|
||||||
|
else:
|
||||||
|
items = items.filter(
|
||||||
|
machine_id__in=selected_machines,
|
||||||
|
status__in=selected_statuses,
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Разбиваем по статусам для вывода в разные таблицы (если они выбраны в фильтре)
|
||||||
|
in_work = items.filter(status='work') if 'work' in selected_statuses else None
|
||||||
|
backlog = items.filter(status='partial') if 'partial' in selected_statuses else None
|
||||||
|
done_items = items.filter(status='done') if 'done' in selected_statuses else None
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'user_role': user_role,
|
||||||
|
'machines': all_machines,
|
||||||
|
'selected_machines': selected_machines,
|
||||||
|
'selected_statuses': selected_statuses,
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'in_work': in_work,
|
||||||
|
'backlog': backlog,
|
||||||
|
'done_items': done_items,
|
||||||
|
}
|
||||||
|
return render(request, 'shiftflow/items_list.html', context)
|
||||||
@@ -1,3 +1,32 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import redirect
|
||||||
|
from django.views.generic import TemplateView, ListView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from .models import Item # Проверь, как точно называется твоя модель деталей/заказов
|
||||||
|
|
||||||
# Create your views here.
|
# Класс главной страницы (роутер)
|
||||||
|
class IndexView(TemplateView):
|
||||||
|
template_name = 'shiftflow/landing.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Если юзер авторизован — сразу отправляем его в реестр
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect('registry')
|
||||||
|
# Если нет — показываем кнопку "Войти"
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Класс реестра деталей (защищен LoginRequiredMixin)
|
||||||
|
class RegistryView(LoginRequiredMixin, ListView):
|
||||||
|
model = Item
|
||||||
|
template_name = 'shiftflow/items_list.html'
|
||||||
|
context_object_name = 'items'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Позже здесь добавим: .filter(machine__in=request.user.profile.machines.all())
|
||||||
|
return Item.objects.all().order_by('-id')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
# Передаем роль в шаблон, чтобы скрывать/показывать кнопки
|
||||||
|
if hasattr(self.request.user, 'profile'):
|
||||||
|
context['user_role'] = self.request.user.profile.role
|
||||||
|
return context
|
||||||
38
static/css/style.css
Normal file
38
static/css/style.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* Акцентные цвета для темной темы */
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
--bs-body-bg: #121212;
|
||||||
|
--bs-body-color: #e9ecef;
|
||||||
|
--bs-accent: #ffc107; /* Тот самый желтый */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .table-custom-header {
|
||||||
|
background-color: #1e1e1e !important;
|
||||||
|
color: var(--bs-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Акцентные цвета для светлой темы */
|
||||||
|
[data-bs-theme="light"] {
|
||||||
|
--bs-body-bg: #e2e2e2;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-accent: #0f5132; /* Темно-зеленый */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Общие классы */
|
||||||
|
.text-accent {
|
||||||
|
color: var(--bs-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-accent {
|
||||||
|
color: var(--bs-accent) !important;
|
||||||
|
border-color: var(--bs-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-accent:hover {
|
||||||
|
background-color: var(--bs-accent) !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Фикс для навигации */
|
||||||
|
.nav-link.active {
|
||||||
|
border-bottom: 2px solid var(--bs-accent);
|
||||||
|
}
|
||||||
47
templates/base.html
Normal file
47
templates/base.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}ShiftFlow MES{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
|
</head>
|
||||||
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% include 'components/_navbar.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<main class="container-fluid py-3 flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% include 'components/_footer.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function updateThemeIcon(theme) {
|
||||||
|
const icon = document.getElementById('theme-icon');
|
||||||
|
if (icon) icon.className = theme === 'dark' ? 'bi bi-brightness-high-fill' : 'bi bi-moon-stars-fill';
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const newTheme = html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-bs-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
updateThemeIcon(newTheme);
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||||
|
updateThemeIcon(savedTheme);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
7
templates/components/_footer.html
Normal file
7
templates/components/_footer.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<footer class="mt-auto py-3 bg-body-tertiary border-top">
|
||||||
|
<div class="container-fluid text-center">
|
||||||
|
<span class="text-muted small">
|
||||||
|
Система учета сменных заданий, разработана <strong>ACK</strong> © 2026
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
57
templates/components/_navbar.html
Normal file
57
templates/components/_navbar.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<nav class="navbar navbar-expand-lg border-bottom shadow-sm">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand fw-bold text-accent" href="/">
|
||||||
|
<i class="bi bi-gear-fill me-2"></i>ShiftFlow
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'items_list' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if user_role in 'admin,technologist' %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#">Планирование</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_role in 'admin,technologist,master,operator' %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#">Закрытие</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_role in 'admin,technologist,clerk' %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#">Списание</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="badge bg-secondary opacity-75 px-3 py-2 fw-normal">
|
||||||
|
<i class="bi bi-person-circle me-1"></i>
|
||||||
|
{% if user_role == 'admin' %}Админ
|
||||||
|
{% elif user_role == 'technologist' %}Технолог
|
||||||
|
{% elif user_role == 'master' %}Мастер
|
||||||
|
{% elif user_role == 'operator' %}Оператор
|
||||||
|
{% elif user_role == 'clerk' %}Учетчик
|
||||||
|
{% endif %}
|
||||||
|
({{ request.user.username|upper }})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if user_role == 'admin' %}
|
||||||
|
<a href="/admin/" class="btn btn-link text-decoration-none text-reset p-0" title="Админка">
|
||||||
|
<i class="bi bi-shield-lock fs-5"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button class="btn btn-link text-reset p-0" onclick="toggleTheme()" title="Сменить тему">
|
||||||
|
<i id="theme-icon" class="bi fs-5"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form action="{% url 'logout' %}" method="post" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-link text-danger p-0 ms-2" title="Выйти">
|
||||||
|
<i class="bi bi-box-arrow-right fs-5"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
40
templates/registration/login.html
Normal file
40
templates/registration/login.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center align-items-center" style="min-height: 70vh;">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-secondary shadow-lg">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="text-accent fw-bold"><i class="bi bi-shield-lock me-2"></i>ВХОД</h2>
|
||||||
|
<p class="text-muted small">Введите ваши данные для доступа к системе</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger small py-2">
|
||||||
|
Неверное имя пользователя или пароль.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Логин</label>
|
||||||
|
<input type="text" name="username" class="form-control form-control-lg border-secondary" placeholder="Имя пользователя" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">Пароль</label>
|
||||||
|
<input type="password" name="password" class="form-control form-control-lg border-secondary" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-outline-accent w-100 py-2 fw-bold shadow-sm">
|
||||||
|
АВТОРИЗОВАТЬСЯ
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user