diff --git a/.env b/.env index ae52174..235bd06 100644 --- a/.env +++ b/.env @@ -1,12 +1,12 @@ -SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms' # Настройки базы данных DB_NAME=prodman_db DB_USER=prodman_user DB_PASS=prodman_password_zwE45t! # Настройки Django -ENV_TYPE=server +ENV_TYPE=dev DB_HOST=db +SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms' # todo потом установить домен для продакшена ALLOWED_HOSTS=192.168.1.108,shiftflow.tertelius.space CSRF_ORIGINS=http://192.168.1.108,https://shiftflow.tertelius.space diff --git a/README.md b/README.md index d088e84..5b56cbb 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,20 @@ ### 💊 Таблетка: Если не пушится (сброс авторизации) Если Git "забыл" пароль или выдает ошибку Permission Denied: -```bash + # Очищаем старые привязки +```bash git config --global --unset credential.helper +``` # Устанавливаем менеджер заново (для Windows) +```bash git config --global credential.helper manager - +``` # Снова пушим и вводим логин/пароль в окне +```bash git push origin main +``` # 🚀 ShiftFlow MES - Инструкция по деплою (Production) diff --git a/core/settings.py b/core/settings.py index 287dbac..b9c6e1a 100644 --- a/core/settings.py +++ b/core/settings.py @@ -19,13 +19,21 @@ import environ BASE_DIR = Path(__file__).resolve().parent.parent 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') # Настройки безопасности -# DEBUG будет True везде, кроме сервера +# DEBUG будет True везде, кроме сервера DEBUG = ENV_TYPE != 'server' @@ -164,9 +172,16 @@ USE_TZ = True STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] + + MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media' +# Куда переходим после логина +LOGIN_REDIRECT_URL = '/' # Куда идем после входа +LOGOUT_REDIRECT_URL = '/' # Куда идем после выхода + # Доверяем прокси-серверу передавать заголовки 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']) + 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}") diff --git a/core/urls.py b/core/urls.py index 9789db1..401dc10 100644 --- a/core/urls.py +++ b/core/urls.py @@ -15,14 +15,19 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf.urls.static import static # <--- Добавьте эту строку from core import settings urlpatterns = [ 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.ENV_TYPE in ['local', 'dev']: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/shiftflow/admin.py b/shiftflow/admin.py index 7ab2fb3..cba828b 100644 --- a/shiftflow/admin.py +++ b/shiftflow/admin.py @@ -1,6 +1,6 @@ import os 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) @@ -11,7 +11,7 @@ class CompanyAdmin(admin.ModelAdmin): # --- Настройка отображения Сделок --- @admin.register(Deal) class DealAdmin(admin.ModelAdmin): - list_display = ('number', 'company', 'created_at') + list_display = ('number', 'company') search_fields = ('number', 'company__name') list_filter = ('company',) @@ -68,4 +68,9 @@ class ItemAdmin(admin.ModelAdmin): return initial # Регистрация станков просто списком -admin.site.register(Machine) \ No newline at end of file +admin.site.register(Machine) + +@admin.register(EmployeeProfile) +class EmployeeProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'role') + filter_horizontal = ('machines',) # Красивый выбор станков двумя колонками \ No newline at end of file diff --git a/shiftflow/migrations/0003_employeeprofile.py b/shiftflow/migrations/0003_employeeprofile.py new file mode 100644 index 0000000..f419c03 --- /dev/null +++ b/shiftflow/migrations/0003_employeeprofile.py @@ -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': 'Профили сотрудников', + }, + ), + ] diff --git a/shiftflow/models.py b/shiftflow/models.py index 9fe45c5..b6b76c3 100644 --- a/shiftflow/models.py +++ b/shiftflow/models.py @@ -94,4 +94,26 @@ class Item(models.Model): def __str__(self): return f"{self.drawing_name} ({self.quantity_plan} шт.)" - \ No newline at end of file + + +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 = 'Профили сотрудников' \ No newline at end of file diff --git a/shiftflow/templates/shiftflow/items_list copy.html b/shiftflow/templates/shiftflow/items_list copy.html new file mode 100644 index 0000000..7ee18e9 --- /dev/null +++ b/shiftflow/templates/shiftflow/items_list copy.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} + +{% block content %} + {% include 'shiftflow/partials/_filter.html' %} + + {% if not selected_machines %} +
+ Станки не выбраны. Выберите станки в фильтре для отображения позиций. +
+ {% else %} + + {% if in_work is not None %} +
В РАБОТЕ
+
+ + + + + + + + + {% if user_role in 'admin,technologist,master,operator' %} + + {% endif %} + + + + {% for item in in_work %} + + + + + + + {% if user_role in 'admin,technologist,master,operator' %} + + {% endif %} + + {% empty %} + + {% endfor %} + +
СделкаДеталь / ЧертежМатериалПланФактДействия
{{ item.deal.number }}{{ item.drawing_name }} ({{ item.machine.name }}){{ item.material.name }}{{ item.quantity_plan }} + + + +
Активных заданий нет
+
+ {% endif %} + + {% if backlog is not None %} +
НЕДОДЕЛЫ (ХВОСТЫ)
+
+
+ {% endif %} + + {% if done_items is not None %} +
ЗАВЕРШЕНО
+
+
+ {% endif %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/shiftflow/templates/shiftflow/items_list.html b/shiftflow/templates/shiftflow/items_list.html new file mode 100644 index 0000000..32c65da --- /dev/null +++ b/shiftflow/templates/shiftflow/items_list.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Реестр деталей

+ {% if user_role == 'admin' or user_role == 'technologist' %} + + {% endif %} +
+
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% empty %} + + + + {% endfor %} + +
IDНаименованиеКол-воСтатус
{{ item.id }}{{ item.name }}{{ item.quantity }}{{ item.status }}
Деталей пока нет
+
+
+{% endblock %} \ No newline at end of file diff --git a/shiftflow/templates/shiftflow/landing.html b/shiftflow/templates/shiftflow/landing.html new file mode 100644 index 0000000..09c0970 --- /dev/null +++ b/shiftflow/templates/shiftflow/landing.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +
+

+ ShiftFlow +

+ + ВОЙТИ В СИСТЕМУ + +
+{% endblock %} \ No newline at end of file diff --git a/shiftflow/templates/shiftflow/partials/_filter.html b/shiftflow/templates/shiftflow/partials/_filter.html new file mode 100644 index 0000000..de4d3ce --- /dev/null +++ b/shiftflow/templates/shiftflow/partials/_filter.html @@ -0,0 +1,45 @@ +
+
+
+
+
Станки:
+
+ {% for m in machines %} +
+ + +
+ {% endfor %} +
+
+ +
+
Статус:
+
+ + + + + + + + +
+
+ +
+ + +
+
+ + +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/shiftflow/urls.py b/shiftflow/urls.py new file mode 100644 index 0000000..bbab240 --- /dev/null +++ b/shiftflow/urls.py @@ -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'), +] \ No newline at end of file diff --git a/shiftflow/views copy.py b/shiftflow/views copy.py new file mode 100644 index 0000000..384b9b6 --- /dev/null +++ b/shiftflow/views copy.py @@ -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) \ No newline at end of file diff --git a/shiftflow/views.py b/shiftflow/views.py index 91ea44a..3fa9b78 100644 --- a/shiftflow/views.py +++ b/shiftflow/views.py @@ -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 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..f763d1c --- /dev/null +++ b/static/css/style.css @@ -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); +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..8a47f62 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,47 @@ +{% load static %} + + + + + + {% block title %}ShiftFlow MES{% endblock %} + + + + + + + {% if user.is_authenticated %} + {% include 'components/_navbar.html' %} + {% endif %} + +
+ {% block content %}{% endblock %} +
+ + {% if user.is_authenticated %} + {% include 'components/_footer.html' %} + {% endif %} + + + + + + \ No newline at end of file diff --git a/templates/components/_footer.html b/templates/components/_footer.html new file mode 100644 index 0000000..7a3912f --- /dev/null +++ b/templates/components/_footer.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/templates/components/_navbar.html b/templates/components/_navbar.html new file mode 100644 index 0000000..5fba7fb --- /dev/null +++ b/templates/components/_navbar.html @@ -0,0 +1,57 @@ + \ No newline at end of file diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..98a98fd --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,40 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

ВХОД

+

Введите ваши данные для доступа к системе

+
+ +
+ {% csrf_token %} + + {% if form.errors %} +
+ Неверное имя пользователя или пароль. +
+ {% endif %} + +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file