commit 9ee249de29e03f3976a68f4265fc2df75d942033 Author: andrey.epifantsev Date: Wed Aug 27 12:47:23 2025 +0400 Init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aed4dd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +# --- OS / Editor --- +.DS_Store +Thumbs.db +*.swp +*.swo +*.orig +*.tmp +*.bak +.idea/ +.vscode/ +*.code-workspace + +# --- Logs --- +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# --- Env / Secrets --- +.env +.env.* +*.env +*.secret +*.key +*.pem + +# --- Node / Angular --- +node_modules/ +frontend/node_modules/ +frontend/dist/ +frontend/.angular/ +frontend/.cache/ +coverage/ +.browserslistrc* + +# --- Python (doc-service) --- +__pycache__/ +*.pyc +*.pyo +*.pyd +*.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ +.envrc +.docz/ +# Outputs +doc-service/app/output/ + +# --- Go (core-service) --- +core-service/bin/ +core-service/tmp/ +*.exe +*.test +*.out +# Go coverage +coverage.out + +# --- Protobuf generated (optional ignore) --- +core-service/proto/**/*.pb.go +# Uncomment if generating stubs for Python/TS +# doc-service/app/proto/** +# frontend/src/app/proto/** + +# --- Docker / Compose --- +**/.docker/ +.dockerignore +# Local data mounts (if any) +docker/postgres/data/ + +# --- Build artifacts --- +dist/ +build/ +.tmp/ +.temp/ + +# --- Misc --- +*.iml +*.DS_Store? +.sass-cache/ +CACHE/ +.cache/ +.npm/ +.parcel-cache/ + +# --- Reports --- +reports/ +*.lcov + +# --- OS Trash --- +$RECYCLE.BIN/ +*.Trash-*/ diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..9c9b936 --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,226 @@ +# 📁 Структура проекта ERP MVP + +``` +erp-mvp/ +├── 📁 core-service/ # Go Backend (Core API) +│ ├── 📁 cmd/ +│ │ └── main.go # Точка входа приложения +│ ├── 📁 internal/ +│ │ ├── 📁 api/ # HTTP API handlers +│ │ ├── 📁 config/ # Конфигурация +│ │ ├── 📁 database/ # Работа с БД +│ │ ├── 📁 grpc/ # gRPC клиенты/серверы +│ │ ├── 📁 logger/ # Логирование +│ │ ├── 📁 models/ # Модели данных +│ │ ├── 📁 redis/ # Redis клиент +│ │ └── 📁 services/ # Бизнес-логика +│ ├── 📁 pkg/ # Публичные пакеты +│ ├── 📁 proto/ # Сгенерированные Protocol Buffers +│ ├── go.mod # Go зависимости +│ ├── go.sum # Go checksums +│ └── Dockerfile # Docker образ +│ +├── 📁 doc-service/ # Python Document Service +│ ├── 📁 app/ +│ │ ├── 📁 api/ # FastAPI роуты +│ │ │ ├── routes/ +│ │ │ └── middleware/ +│ │ ├── 📁 core/ # Основная логика +│ │ │ ├── redis_client.py +│ │ │ └── logging.py +│ │ ├── 📁 models/ # Pydantic модели +│ │ │ └── document.py +│ │ ├── 📁 services/ # Сервисы генерации документов +│ │ │ ├── pdf_service.py +│ │ │ ├── excel_service.py +│ │ │ └── qr_service.py +│ │ ├── 📁 templates/ # Jinja2 шаблоны +│ │ ├── config.py # Конфигурация +│ │ └── main.py # Точка входа +│ ├── 📁 output/ # Генерируемые документы +│ ├── requirements.txt # Python зависимости +│ └── Dockerfile # Docker образ +│ +├── 📁 frontend/ # Angular PWA Frontend +│ ├── 📁 src/ +│ │ ├── 📁 app/ +│ │ │ ├── 📁 components/ # Angular компоненты +│ │ │ │ ├── header/ +│ │ │ │ ├── qr-scanner/ +│ │ │ │ ├── location-details/ +│ │ │ │ ├── item-list/ +│ │ │ │ └── search/ +│ │ │ ├── 📁 services/ # Angular сервисы +│ │ │ │ ├── api.service.ts +│ │ │ │ ├── auth.service.ts +│ │ │ │ └── auth.interceptor.ts +│ │ │ ├── 📁 store/ # NgRx store +│ │ │ │ ├── app.reducers.ts +│ │ │ │ ├── app.effects.ts +│ │ │ │ └── app.actions.ts +│ │ │ ├── 📁 models/ # TypeScript модели +│ │ │ ├── app.component.ts +│ │ │ ├── app.module.ts +│ │ │ └── app-routing.module.ts +│ │ ├── 📁 assets/ # Статические ресурсы +│ │ ├── 📁 environments/ # Конфигурация окружений +│ │ ├── index.html +│ │ ├── main.ts +│ │ ├── styles.scss +│ │ └── manifest.webmanifest # PWA манифест +│ ├── package.json # Node.js зависимости +│ ├── angular.json # Angular конфигурация +│ ├── nginx.conf # Nginx конфигурация +│ └── Dockerfile # Docker образ +│ +├── 📁 proto/ # Protocol Buffers +│ ├── core.proto # Core Service API +│ ├── document.proto # Document Service API +│ └── events.proto # Event-driven сообщения +│ +├── 📁 docker/ # Docker конфигурации +│ ├── 📁 postgres/ +│ │ └── init.sql # Инициализация БД +│ ├── 📁 prometheus/ +│ │ └── prometheus.yml # Prometheus конфигурация +│ └── 📁 grafana/ +│ └── provisioning/ # Grafana дашборды +│ +├── docker-compose.yml # Основной compose файл +├── README.md # Основная документация +├── STARTUP.md # Инструкции по запуску +└── PROJECT_STRUCTURE.md # Этот файл +``` + +## 🔧 Технологический стек + +### Core Service (Go) +- **Framework:** Gin (HTTP сервер) +- **Database:** PostgreSQL (основная БД) +- **Cache:** Redis (кэширование) +- **Auth:** JWT (аутентификация) +- **gRPC:** Protocol Buffers (межсервисная коммуникация) +- **Logging:** logrus/zerolog (структурированное логирование) +- **Monitoring:** Prometheus (метрики) + +### Document Service (Python) +- **Framework:** FastAPI (веб-фреймворк) +- **PDF:** reportlab, weasyprint (генерация PDF) +- **Excel:** openpyxl (работа с Excel) +- **Word:** python-docx (работа с Word) +- **QR:** qrcode (генерация QR-кодов) +- **Templates:** Jinja2 (шаблонизация) +- **Cache:** Redis (кэширование документов) +- **gRPC:** grpcio (межсервисная коммуникация) + +### Frontend (Angular) +- **Framework:** Angular 17+ (SPA фреймворк) +- **PWA:** @angular/service-worker (Progressive Web App) +- **UI:** Angular Material (компоненты UI) +- **Styling:** Tailwind CSS (утилитарные стили) +- **QR Scanner:** @zxing/ngx-scanner (сканирование QR) +- **State:** NgRx (управление состоянием) +- **HTTP:** HttpClient (API запросы) + +### Infrastructure +- **Containerization:** Docker + Docker Compose +- **Database:** PostgreSQL 15 (основная БД) +- **Cache:** Redis 7 (кэширование) +- **Gateway:** Traefik (опционально) +- **Monitoring:** Prometheus + Grafana (опционально) +- **Web Server:** Nginx (для frontend) + +## 🚀 Архитектура + +### Микросервисная архитектура +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Angular PWA │ │ Go Core │ │ PostgreSQL │ +│ (Frontend) │◄──►│ Service │◄──►│ (Database) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + │ │ + ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ +│ QR Scanner │ │ Python Doc │ +│ (Camera) │ │ Service │ +└─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Redis Cache │ + │ (Documents) │ + └─────────────────┘ +``` + +### Коммуникация между сервисами +- **Frontend ↔ Core Service:** REST API (HTTP/JSON) +- **Frontend ↔ Document Service:** REST API (HTTP/JSON) +- **Core Service ↔ Document Service:** gRPC (Protocol Buffers) +- **Все сервисы ↔ Redis:** Redis Protocol (кэширование) + +## 📊 База данных + +### Основные таблицы +- **organizations** - Организации/компании +- **users** - Пользователи системы +- **storage_locations** - Места хранения +- **items** - Товары/материалы +- **item_placements** - Размещение товаров +- **operations** - Операции с товарами + +### Индексы +- Организация-scope индексы для всех таблиц +- Индексы по типам и категориям +- Временные индексы для аналитики + +## 🔐 Безопасность + +### Аутентификация +- JWT токены с refresh механизмом +- Organization-scope на всех данных +- Middleware для проверки прав доступа + +### Валидация +- Входная валидация всех параметров +- SQL injection protection +- XSS protection в frontend + +### HTTPS +- Обязательное использование HTTPS в продакшене +- Secure cookies для JWT +- CORS настройки для PWA + +## 📱 PWA Особенности + +### Service Worker +- Кэширование статических ресурсов +- Offline fallback для базовых функций +- Background sync для операций + +### QR Scanner +- WebRTC для доступа к камере +- Real-time распознавание QR-кодов +- Fallback на ручной ввод + +### Установка +- Manifest для установки как нативное приложение +- Splash screen и иконки +- Автоматические обновления + +## 🚀 Развертывание + +### Docker Compose +- Все сервисы в отдельных контейнерах +- Автоматическая инициализация БД +- Готовые образы для быстрого старта + +### Мониторинг +- Prometheus для сбора метрик +- Grafana для визуализации +- Structured logging для всех сервисов + +### Масштабирование +- Горизонтальное масштабирование сервисов +- Load balancing через Traefik +- Кэширование через Redis diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ad5562 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# ERP для мастеров - MVP + +## 🏗️ Архитектура проекта + +``` +erp-mvp/ +├── core-service/ # Go backend (Core API) +├── doc-service/ # Python document service +├── frontend/ # Angular PWA frontend +├── proto/ # Shared Protocol Buffers +├── docker/ # Docker configurations +└── docker-compose.yml # Main deployment file +``` + +## 🚀 Быстрый старт + +### Требования +- Docker & Docker Compose +- Go 1.21+ +- Node.js 18+ +- Python 3.11+ + +### Запуск +```bash +# Клонирование и настройка +git clone +cd erp-mvp + +# Запуск всех сервисов +docker-compose up -d + +# Или разработка локально +cd core-service && go run cmd/main.go +cd doc-service && python -m uvicorn app.main:app --reload +cd frontend && npm start +``` + +## 📚 Документация + +- [Архитектура MVP](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/Архитектура-MVP.md) +- [Техническое задание](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/Техническое-задание-MVP.md) +- [MVP План](second-mind-aep/💡%20Идеи/💡%20Проекты/ERP%20для%20малых%20производств/MVP-План.md) + +## 🔧 Технологический стек + +- **Core Service:** Go (Gin) + PostgreSQL + JWT + gRPC +- **Document Service:** Python (FastAPI) + Redis + Document libraries +- **Frontend:** Angular PWA + Material UI + Tailwind CSS +- **Infrastructure:** Docker + Docker Compose + Redis + HTTPS diff --git a/STARTUP.md b/STARTUP.md new file mode 100644 index 0000000..8d0540f --- /dev/null +++ b/STARTUP.md @@ -0,0 +1,246 @@ +# 🚀 Инструкции по запуску ERP MVP + +## 📋 Требования + +### Системные требования +- **Docker** 20.10+ +- **Docker Compose** 2.0+ +- **Git** 2.30+ + +### Минимальные ресурсы +- **RAM:** 4 GB +- **CPU:** 2 ядра +- **Диск:** 10 GB свободного места + +## 🔧 Быстрый запуск + +### 1. Клонирование репозитория +```bash +git clone +cd erp-mvp +``` + +### 2. Настройка переменных окружения +```bash +# Создание .env файла для Core Service +cp core-service/.env.example core-service/.env + +# Создание .env файла для Document Service +cp doc-service/.env.example doc-service/.env + +# Редактирование переменных (опционально) +nano core-service/.env +nano doc-service/.env +``` + +### 3. Запуск всех сервисов +```bash +# Сборка и запуск всех контейнеров +docker-compose up -d + +# Просмотр логов +docker-compose logs -f + +# Проверка статуса сервисов +docker-compose ps +``` + +### 4. Проверка работоспособности +```bash +# Core Service API +curl http://localhost:8080/health + +# Document Service API +curl http://localhost:8000/health + +# Frontend +curl http://localhost:3000/health +``` + +## 🌐 Доступ к сервисам + +После успешного запуска: + +- **Frontend (Angular PWA):** http://localhost:3000 +- **Core Service API:** http://localhost:8080 +- **Document Service API:** http://localhost:8000 +- **API Documentation:** http://localhost:8000/docs +- **PostgreSQL:** localhost:5432 +- **Redis:** localhost:6379 + +## 🔍 Мониторинг (опционально) + +### Запуск с мониторингом +```bash +# Запуск с Prometheus и Grafana +docker-compose --profile monitoring up -d + +# Доступ к Grafana: http://localhost:3001 +# Логин: admin / admin +``` + +### Запуск с API Gateway +```bash +# Запуск с Traefik +docker-compose --profile gateway up -d + +# Traefik Dashboard: http://localhost:8081 +``` + +## 🛠️ Разработка + +### Локальная разработка + +#### Core Service (Go) +```bash +cd core-service +go mod download +go run cmd/main.go +``` + +#### Document Service (Python) +```bash +cd doc-service +pip install -r requirements.txt +python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +#### Frontend (Angular) +```bash +cd frontend +npm install +npm start +``` + +### Горячая перезагрузка +```bash +# Пересборка и перезапуск конкретного сервиса +docker-compose up -d --build core-service + +# Пересборка всех сервисов +docker-compose up -d --build +``` + +## 🗄️ База данных + +### Подключение к PostgreSQL +```bash +# Через Docker +docker-compose exec postgres psql -U erp_user -d erp_mvp + +# Или через внешний клиент +# Host: localhost +# Port: 5432 +# Database: erp_mvp +# Username: erp_user +# Password: erp_pass +``` + +### Миграции +```bash +# Применение миграций (если есть) +docker-compose exec core-service ./migrate up +``` + +## 🔐 Безопасность + +### Изменение паролей по умолчанию +```bash +# В docker-compose.yml измените: +# - POSTGRES_PASSWORD +# - JWT_SECRET +# - REDIS_PASSWORD (если используется) +``` + +### HTTPS (для продакшена) +```bash +# Добавьте SSL сертификаты +# Настройте Traefik для HTTPS +# Обновите переменные окружения +``` + +## 🐛 Устранение неполадок + +### Просмотр логов +```bash +# Все сервисы +docker-compose logs + +# Конкретный сервис +docker-compose logs core-service +docker-compose logs doc-service +docker-compose logs frontend + +# Следить за логами в реальном времени +docker-compose logs -f +``` + +### Перезапуск сервисов +```bash +# Перезапуск конкретного сервиса +docker-compose restart core-service + +# Перезапуск всех сервисов +docker-compose restart +``` + +### Очистка данных +```bash +# Остановка и удаление контейнеров +docker-compose down + +# Удаление с данными +docker-compose down -v + +# Полная очистка +docker-compose down -v --rmi all +``` + +## 📊 Проверка работоспособности + +### Тестовые запросы +```bash +# Проверка Core Service +curl -X GET http://localhost:8080/health + +# Проверка Document Service +curl -X GET http://localhost:8000/health + +# Проверка Frontend +curl -X GET http://localhost:3000/health +``` + +### Проверка базы данных +```bash +# Подключение к PostgreSQL +docker-compose exec postgres psql -U erp_user -d erp_mvp -c "SELECT version();" + +# Проверка таблиц +docker-compose exec postgres psql -U erp_user -d erp_mvp -c "\dt" +``` + +## 🚀 Продакшен развертывание + +### Подготовка к продакшену +1. Измените все пароли по умолчанию +2. Настройте SSL сертификаты +3. Настройте мониторинг +4. Настройте бэкапы +5. Обновите переменные окружения + +### Команды для продакшена +```bash +# Сборка оптимизированных образов +docker-compose -f docker-compose.yml -f docker-compose.prod.yml build + +# Запуск в продакшене +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d +``` + +## 📞 Поддержка + +При возникновении проблем: +1. Проверьте логи: `docker-compose logs` +2. Убедитесь, что все порты свободны +3. Проверьте системные ресурсы +4. Обратитесь к документации проекта diff --git a/core-service/Dockerfile b/core-service/Dockerfile new file mode 100644 index 0000000..5b452b5 --- /dev/null +++ b/core-service/Dockerfile @@ -0,0 +1,51 @@ +# Многоэтапная сборка для Go приложения +FROM golang:1.21-alpine AS builder + +# Установка зависимостей для сборки +RUN apk add --no-cache git ca-certificates tzdata + +# Установка рабочей директории +WORKDIR /app + +# Копирование go mod файлов +COPY go.mod go.sum ./ + +# Скачивание зависимостей +RUN go mod download + +# Копирование исходного кода +COPY . . + +# Сборка приложения +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/main.go + +# Финальный образ +FROM alpine:latest + +# Установка ca-certificates для HTTPS запросов +RUN apk --no-cache add ca-certificates tzdata + +# Создание пользователя для безопасности +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Установка рабочей директории +WORKDIR /root/ + +# Копирование бинарного файла из builder +COPY --from=builder /app/main . + +# Копирование конфигурационных файлов +COPY --from=builder /app/config ./config + +# Смена владельца файлов +RUN chown -R appuser:appgroup /root/ + +# Переключение на непривилегированного пользователя +USER appuser + +# Экспорт порта +EXPOSE 8080 + +# Команда запуска +CMD ["./main"] diff --git a/core-service/cmd/main.go b/core-service/cmd/main.go new file mode 100644 index 0000000..95dea32 --- /dev/null +++ b/core-service/cmd/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "erp-mvp/core-service/internal/api" + "erp-mvp/core-service/internal/config" + "erp-mvp/core-service/internal/database" + "erp-mvp/core-service/internal/grpc" + "erp-mvp/core-service/internal/logger" + "erp-mvp/core-service/internal/redis" +) + +func main() { + // Инициализация логгера + logger := logger.New() + + // Загрузка конфигурации + cfg, err := config.Load() + if err != nil { + logger.Fatal("Failed to load config", err) + } + + // Подключение к базе данных + db, err := database.Connect(cfg.Database) + if err != nil { + logger.Fatal("Failed to connect to database", err) + } + defer db.Close() + + // Подключение к Redis + redisClient, err := redis.Connect(cfg.Redis) + if err != nil { + logger.Fatal("Failed to connect to Redis", err) + } + defer redisClient.Close() + + // Инициализация gRPC клиента для Document Service + grpcClient, err := grpc.NewDocumentServiceClient(cfg.DocumentService.URL) + if err != nil { + logger.Fatal("Failed to connect to Document Service", err) + } + defer grpcClient.Close() + + // Создание API сервера + server := api.NewServer(cfg, db, redisClient, grpcClient, logger) + + // Запуск HTTP сервера + go func() { + logger.Info("Starting HTTP server on", cfg.Server.Port) + if err := server.Start(); err != nil && err != http.ErrServerClosed { + logger.Fatal("Failed to start server", err) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logger.Info("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + logger.Fatal("Server forced to shutdown", err) + } + + logger.Info("Server exited") +} diff --git a/core-service/go.mod b/core-service/go.mod new file mode 100644 index 0000000..7bfbb41 --- /dev/null +++ b/core-service/go.mod @@ -0,0 +1,47 @@ +module erp-mvp/core-service + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/lib/pq v1.10.9 + github.com/go-playground/validator/v10 v10.15.5 + google.golang.org/grpc v1.58.0 + github.com/swaggo/gin-swagger v1.6.0 + github.com/redis/go-redis/v9 v9.2.1 + github.com/google/uuid v1.3.1 + github.com/joho/godotenv v1.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/prometheus/client_golang v1.17.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/core-service/internal/config/config.go b/core-service/internal/config/config.go new file mode 100644 index 0000000..ae78874 --- /dev/null +++ b/core-service/internal/config/config.go @@ -0,0 +1,103 @@ +package config + +import ( + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + Redis RedisConfig + JWT JWTConfig + DocumentService DocumentServiceConfig + Log LogConfig +} + +type ServerConfig struct { + Port string + Host string +} + +type DatabaseConfig struct { + Host string + Port string + User string + Password string + DBName string + SSLMode string +} + +type RedisConfig struct { + Host string + Port string + Password string + DB int +} + +type JWTConfig struct { + Secret string + Expiration int // в часах +} + +type DocumentServiceConfig struct { + URL string +} + +type LogConfig struct { + Level string +} + +func Load() (*Config, error) { + // Загрузка .env файла если существует + godotenv.Load() + + return &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Host: getEnv("SERVER_HOST", "0.0.0.0"), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "5432"), + User: getEnv("DB_USER", "erp_user"), + Password: getEnv("DB_PASSWORD", "erp_pass"), + DBName: getEnv("DB_NAME", "erp_mvp"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + }, + Redis: RedisConfig{ + Host: getEnv("REDIS_HOST", "localhost"), + Port: getEnv("REDIS_PORT", "6379"), + Password: getEnv("REDIS_PASSWORD", ""), + DB: getEnvAsInt("REDIS_DB", 0), + }, + JWT: JWTConfig{ + Secret: getEnv("JWT_SECRET", "your-secret-key"), + Expiration: getEnvAsInt("JWT_EXPIRATION", 24), + }, + DocumentService: DocumentServiceConfig{ + URL: getEnv("DOC_SERVICE_URL", "http://localhost:8000"), + }, + Log: LogConfig{ + Level: getEnv("LOG_LEVEL", "info"), + }, + }, nil +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvAsInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} diff --git a/core-service/internal/models/models.go b/core-service/internal/models/models.go new file mode 100644 index 0000000..376bc98 --- /dev/null +++ b/core-service/internal/models/models.go @@ -0,0 +1,120 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +// Organization представляет организацию/компанию +type Organization struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Type string `json:"type" db:"type"` + Settings map[string]any `json:"settings" db:"settings"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// User представляет пользователя системы +type User struct { + ID uuid.UUID `json:"id" db:"id"` + OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` + Email string `json:"email" db:"email"` + Role string `json:"role" db:"role"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// StorageLocation представляет место хранения +type StorageLocation struct { + ID uuid.UUID `json:"id" db:"id"` + OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` + ParentID *uuid.UUID `json:"parent_id" db:"parent_id"` + Name string `json:"name" db:"name"` + Address string `json:"address" db:"address"` + Type string `json:"type" db:"type"` + Coordinates map[string]any `json:"coordinates" db:"coordinates"` + QRCode string `json:"qr_code" db:"qr_code"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// Item представляет товар/материал +type Item struct { + ID uuid.UUID `json:"id" db:"id"` + OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Category string `json:"category" db:"category"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// ItemPlacement представляет размещение товара в месте хранения +type ItemPlacement struct { + ID uuid.UUID `json:"id" db:"id"` + OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"` + ItemID uuid.UUID `json:"item_id" db:"item_id"` + LocationID uuid.UUID `json:"location_id" db:"location_id"` + Quantity int `json:"quantity" db:"quantity"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// LoginRequest запрос на аутентификацию +type LoginRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse ответ на аутентификацию +type LoginResponse struct { + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + User User `json:"user"` + ExpiresAt time.Time `json:"expires_at"` +} + +// CreateLocationRequest запрос на создание места хранения +type CreateLocationRequest struct { + Name string `json:"name" binding:"required"` + Address string `json:"address" binding:"required"` + Type string `json:"type" binding:"required"` + ParentID *uuid.UUID `json:"parent_id"` + Coordinates map[string]any `json:"coordinates"` +} + +// CreateItemRequest запрос на создание товара +type CreateItemRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category" binding:"required"` +} + +// PlaceItemRequest запрос на размещение товара +type PlaceItemRequest struct { + ItemID uuid.UUID `json:"item_id" binding:"required"` + LocationID uuid.UUID `json:"location_id" binding:"required"` + Quantity int `json:"quantity" binding:"required,min=1"` +} + +// SearchRequest запрос на поиск +type SearchRequest struct { + Query string `json:"query" binding:"required"` + Category string `json:"category"` + LocationID *uuid.UUID `json:"location_id"` +} + +// SearchResponse результат поиска +type SearchResponse struct { + Items []ItemWithLocation `json:"items"` + TotalCount int `json:"total_count"` +} + +// ItemWithLocation товар с информацией о месте размещения +type ItemWithLocation struct { + Item Item `json:"item"` + Location StorageLocation `json:"location"` + Quantity int `json:"quantity"` +} diff --git a/doc-service/Dockerfile b/doc-service/Dockerfile new file mode 100644 index 0000000..26eb8bb --- /dev/null +++ b/doc-service/Dockerfile @@ -0,0 +1,53 @@ +# Многоэтапная сборка для Python приложения +FROM python:3.11-slim AS builder + +# Установка зависимостей для сборки +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Установка рабочей директории +WORKDIR /app + +# Копирование requirements файла +COPY requirements.txt . + +# Установка зависимостей Python +RUN pip install --no-cache-dir --user -r requirements.txt + +# Финальный образ +FROM python:3.11-slim + +# Установка системных зависимостей +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Создание пользователя для безопасности +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Установка рабочей директории +WORKDIR /app + +# Копирование Python пакетов из builder +COPY --from=builder /root/.local /root/.local + +# Копирование исходного кода +COPY . . + +# Создание необходимых директорий +RUN mkdir -p /app/templates /app/output && \ + chown -R appuser:appuser /app + +# Добавление локальных пакетов в PATH +ENV PATH=/root/.local/bin:$PATH + +# Переключение на непривилегированного пользователя +USER appuser + +# Экспорт порта +EXPOSE 8000 + +# Команда запуска +CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/doc-service/app/config.py b/doc-service/app/config.py new file mode 100644 index 0000000..992461e --- /dev/null +++ b/doc-service/app/config.py @@ -0,0 +1,56 @@ +import os +from typing import List +from pydantic_settings import BaseSettings +from dotenv import load_dotenv + +# Загрузка .env файла +load_dotenv() + +class Settings(BaseSettings): + """Настройки приложения""" + + # Основные настройки + DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true" + HOST: str = os.getenv("HOST", "0.0.0.0") + PORT: int = int(os.getenv("PORT", "8000")) + LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") + + # CORS + ALLOWED_ORIGINS: List[str] = [ + "http://localhost:3000", + "http://localhost:8080", + "https://localhost:3000", + "https://localhost:8080" + ] + + # Redis + REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost") + REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379")) + REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "") + REDIS_DB: int = int(os.getenv("REDIS_DB", "0")) + + # Core Service + CORE_SERVICE_URL: str = os.getenv("CORE_SERVICE_URL", "http://localhost:8080") + + # Документы + DOCUMENTS_CACHE_TTL: int = int(os.getenv("DOCUMENTS_CACHE_TTL", "86400")) # 24 часа + MAX_DOCUMENT_SIZE: int = int(os.getenv("MAX_DOCUMENT_SIZE", "10485760")) # 10MB + + # Пути для файлов + TEMPLATES_DIR: str = os.getenv("TEMPLATES_DIR", "app/templates") + OUTPUT_DIR: str = os.getenv("OUTPUT_DIR", "app/output") + + # QR коды + QR_CODE_SIZE: int = int(os.getenv("QR_CODE_SIZE", "10")) + QR_CODE_BORDER: int = int(os.getenv("QR_CODE_BORDER", "2")) + + class Config: + env_file = ".env" + case_sensitive = False + +# Создание экземпляра настроек +settings = Settings() + +# Создание директорий если не существуют +os.makedirs(settings.TEMPLATES_DIR, exist_ok=True) +os.makedirs(settings.OUTPUT_DIR, exist_ok=True) diff --git a/doc-service/app/main.py b/doc-service/app/main.py new file mode 100644 index 0000000..83eeaa7 --- /dev/null +++ b/doc-service/app/main.py @@ -0,0 +1,80 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +import structlog + +from app.config import settings +from app.api.routes import documents, templates +from app.core.redis_client import redis_client +from app.core.logging import setup_logging + +# Настройка логирования +setup_logging() +logger = structlog.get_logger() + +# Создание FastAPI приложения +app = FastAPI( + title="ERP Document Service", + description="Сервис для генерации документов (PDF, Excel, Word)", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# Настройка CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Подключение роутов +app.include_router(documents.router, prefix="/api/documents", tags=["documents"]) +app.include_router(templates.router, prefix="/api/templates", tags=["templates"]) + +@app.on_event("startup") +async def startup_event(): + """Событие запуска приложения""" + logger.info("Starting Document Service") + + # Подключение к Redis + await redis_client.connect() + logger.info("Connected to Redis") + +@app.on_event("shutdown") +async def shutdown_event(): + """Событие остановки приложения""" + logger.info("Shutting down Document Service") + + # Отключение от Redis + await redis_client.disconnect() + logger.info("Disconnected from Redis") + +@app.get("/health") +async def health_check(): + """Проверка здоровья сервиса""" + return { + "status": "healthy", + "service": "document-service", + "version": "1.0.0" + } + +@app.get("/") +async def root(): + """Корневой эндпоинт""" + return { + "message": "ERP Document Service", + "docs": "/docs", + "health": "/health" + } + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=settings.HOST, + port=settings.PORT, + reload=settings.DEBUG, + log_level=settings.LOG_LEVEL.lower() + ) diff --git a/doc-service/app/models/document.py b/doc-service/app/models/document.py new file mode 100644 index 0000000..ae27a02 --- /dev/null +++ b/doc-service/app/models/document.py @@ -0,0 +1,86 @@ +from typing import Optional, Dict, Any, List +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +class DocumentType(str, Enum): + """Типы документов""" + PDF = "pdf" + EXCEL = "excel" + WORD = "word" + QR_CODE = "qr_code" + +class DocumentStatus(str, Enum): + """Статусы документов""" + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + +class QRCodeRequest(BaseModel): + """Запрос на генерацию QR-кода""" + location_id: str = Field(..., description="ID места хранения") + location_address: str = Field(..., description="Адрес места") + organization_id: str = Field(..., description="ID организации") + size: Optional[int] = Field(10, description="Размер QR-кода") + border: Optional[int] = Field(2, description="Размер границы") + +class QRCodeResponse(BaseModel): + """Ответ с QR-кодом""" + document_id: str = Field(..., description="ID документа") + qr_code_url: str = Field(..., description="URL для скачивания QR-кода") + qr_code_data: str = Field(..., description="Данные QR-кода") + expires_at: datetime = Field(..., description="Время истечения") + +class ReportRequest(BaseModel): + """Запрос на генерацию отчета""" + report_type: str = Field(..., description="Тип отчета") + organization_id: str = Field(..., description="ID организации") + filters: Optional[Dict[str, Any]] = Field({}, description="Фильтры для отчета") + format: DocumentType = Field(DocumentType.PDF, description="Формат отчета") + +class ReportResponse(BaseModel): + """Ответ с отчетом""" + document_id: str = Field(..., description="ID документа") + download_url: str = Field(..., description="URL для скачивания") + file_size: int = Field(..., description="Размер файла в байтах") + expires_at: datetime = Field(..., description="Время истечения") + +class DocumentStatusResponse(BaseModel): + """Ответ со статусом документа""" + document_id: str = Field(..., description="ID документа") + status: DocumentStatus = Field(..., description="Статус документа") + progress: Optional[int] = Field(None, description="Прогресс в процентах") + error_message: Optional[str] = Field(None, description="Сообщение об ошибке") + created_at: datetime = Field(..., description="Время создания") + updated_at: datetime = Field(..., description="Время обновления") + +class GeneratePDFRequest(BaseModel): + """Запрос на генерацию PDF""" + template_name: str = Field(..., description="Название шаблона") + data: Dict[str, Any] = Field(..., description="Данные для шаблона") + filename: Optional[str] = Field(None, description="Имя файла") + +class GenerateExcelRequest(BaseModel): + """Запрос на генерацию Excel""" + data: List[Dict[str, Any]] = Field(..., description="Данные для таблицы") + sheet_name: str = Field("Sheet1", description="Название листа") + filename: Optional[str] = Field(None, description="Имя файла") + +class TemplateInfo(BaseModel): + """Информация о шаблоне""" + name: str = Field(..., description="Название шаблона") + description: str = Field(..., description="Описание шаблона") + variables: List[str] = Field(..., description="Переменные шаблона") + created_at: datetime = Field(..., description="Время создания") + updated_at: datetime = Field(..., description="Время обновления") + +class DocumentInfo(BaseModel): + """Информация о документе""" + id: str = Field(..., description="ID документа") + type: DocumentType = Field(..., description="Тип документа") + status: DocumentStatus = Field(..., description="Статус документа") + filename: str = Field(..., description="Имя файла") + file_size: int = Field(..., description="Размер файла") + created_at: datetime = Field(..., description="Время создания") + expires_at: datetime = Field(..., description="Время истечения") diff --git a/doc-service/requirements.txt b/doc-service/requirements.txt new file mode 100644 index 0000000..ca16808 --- /dev/null +++ b/doc-service/requirements.txt @@ -0,0 +1,43 @@ +# Web framework +fastapi==0.104.1 +uvicorn==0.24.0 + +# PDF generation +reportlab==4.0.4 +weasyprint==60.1 + +# Office documents +python-docx==1.1.0 +openpyxl==3.1.2 + +# Templates +jinja2==3.1.2 + +# Redis client +redis==5.0.1 + +# gRPC +grpcio==1.59.0 +protobuf==4.24.4 + +# Data validation +pydantic==2.4.2 + +# Environment variables +python-dotenv==1.0.0 + +# Logging +structlog==23.2.0 + +# QR code generation +qrcode==7.4.2 +Pillow==10.0.1 + +# HTTP client +httpx==0.25.1 + +# CORS +fastapi-cors==0.0.6 + +# Monitoring +prometheus-client==0.19.0 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..24daeea --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,179 @@ +version: '3.8' + +services: + # Core Service (Go) + core-service: + build: + context: ./core-service + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=8080 + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=erp_user + - DB_PASSWORD=erp_pass + - DB_NAME=erp_mvp + - DB_SSLMODE=disable + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD= + - REDIS_DB=0 + - JWT_SECRET=your-super-secret-jwt-key-change-in-production + - JWT_EXPIRATION=24 + - DOC_SERVICE_URL=http://doc-service:8000 + - LOG_LEVEL=info + depends_on: + - postgres + - redis + networks: + - erp-network + restart: unless-stopped + + # Document Service (Python) + doc-service: + build: + context: ./doc-service + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + - HOST=0.0.0.0 + - PORT=8000 + - DEBUG=false + - LOG_LEVEL=INFO + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD= + - REDIS_DB=1 + - CORE_SERVICE_URL=http://core-service:8080 + - DOCUMENTS_CACHE_TTL=86400 + - MAX_DOCUMENT_SIZE=10485760 + - TEMPLATES_DIR=/app/templates + - OUTPUT_DIR=/app/output + - QR_CODE_SIZE=10 + - QR_CODE_BORDER=2 + volumes: + - doc_templates:/app/templates + - doc_output:/app/output + depends_on: + - redis + networks: + - erp-network + restart: unless-stopped + + # Frontend (Angular) + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:80" + environment: + - API_URL=http://localhost:8080 + - DOC_SERVICE_URL=http://localhost:8000 + depends_on: + - core-service + networks: + - erp-network + restart: unless-stopped + + # Database (PostgreSQL) + postgres: + image: postgres:15-alpine + ports: + - "5432:5432" + environment: + - POSTGRES_DB=erp_mvp + - POSTGRES_USER=erp_user + - POSTGRES_PASSWORD=erp_pass + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - erp-network + restart: unless-stopped + + # Cache (Redis) + redis: + image: redis:7-alpine + ports: + - "6379:6379" + command: redis-server --appendonly yes + volumes: + - redis_data:/data + networks: + - erp-network + restart: unless-stopped + + # API Gateway (Traefik) - опционально + traefik: + image: traefik:v2.10 + command: + - --api.insecure=true + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + ports: + - "80:80" + - "443:443" + - "8081:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + networks: + - erp-network + restart: unless-stopped + profiles: + - gateway + + # Monitoring (Prometheus) + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--storage.tsdb.retention.time=200h' + - '--web.enable-lifecycle' + networks: + - erp-network + restart: unless-stopped + profiles: + - monitoring + + # Monitoring (Grafana) + grafana: + image: grafana/grafana:latest + ports: + - "3001:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana_data:/var/lib/grafana + - ./docker/grafana/provisioning:/etc/grafana/provisioning + networks: + - erp-network + restart: unless-stopped + profiles: + - monitoring + +volumes: + postgres_data: + redis_data: + doc_templates: + doc_output: + prometheus_data: + grafana_data: + +networks: + erp-network: + driver: bridge diff --git a/docker/postgres/init.sql b/docker/postgres/init.sql new file mode 100644 index 0000000..26fc09a --- /dev/null +++ b/docker/postgres/init.sql @@ -0,0 +1,124 @@ +-- Инициализация базы данных ERP MVP + +-- Создание расширений +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Создание таблиц + +-- Организации +CREATE TABLE IF NOT EXISTS organizations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + type VARCHAR(100), + settings JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Пользователи +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(50) DEFAULT 'user', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Места хранения +CREATE TABLE IF NOT EXISTS storage_locations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + parent_id UUID REFERENCES storage_locations(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + address VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + coordinates JSONB DEFAULT '{}', + qr_code VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Товары +CREATE TABLE IF NOT EXISTS items ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + category VARCHAR(100), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Размещение товаров +CREATE TABLE IF NOT EXISTS item_placements ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, + location_id UUID NOT NULL REFERENCES storage_locations(id) ON DELETE CASCADE, + quantity INTEGER DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Операции +CREATE TABLE IF NOT EXISTS operations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + operation_type VARCHAR(50) NOT NULL, + item_id UUID REFERENCES items(id) ON DELETE SET NULL, + from_location_id UUID REFERENCES storage_locations(id) ON DELETE SET NULL, + to_location_id UUID REFERENCES storage_locations(id) ON DELETE SET NULL, + quantity INTEGER, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Создание индексов для производительности +CREATE INDEX IF NOT EXISTS idx_users_organization_id ON users(organization_id); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_storage_locations_organization_id ON storage_locations(organization_id); +CREATE INDEX IF NOT EXISTS idx_storage_locations_parent_id ON storage_locations(parent_id); +CREATE INDEX IF NOT EXISTS idx_storage_locations_type ON storage_locations(type); +CREATE INDEX IF NOT EXISTS idx_items_organization_id ON items(organization_id); +CREATE INDEX IF NOT EXISTS idx_items_category ON items(category); +CREATE INDEX IF NOT EXISTS idx_item_placements_organization_id ON item_placements(organization_id); +CREATE INDEX IF NOT EXISTS idx_item_placements_item_id ON item_placements(item_id); +CREATE INDEX IF NOT EXISTS idx_item_placements_location_id ON item_placements(location_id); +CREATE INDEX IF NOT EXISTS idx_operations_organization_id ON operations(organization_id); +CREATE INDEX IF NOT EXISTS idx_operations_created_at ON operations(created_at); + +-- Создание триггеров для автоматического обновления updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_storage_locations_updated_at BEFORE UPDATE ON storage_locations + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_items_updated_at BEFORE UPDATE ON items + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_item_placements_updated_at BEFORE UPDATE ON item_placements + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Создание тестовых данных (опционально) +INSERT INTO organizations (id, name, type) VALUES + ('550e8400-e29b-41d4-a716-446655440000', 'Тестовая мастерская', 'workshop') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO users (id, organization_id, email, password_hash, role) VALUES + ('550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440000', 'admin@test.com', '$2a$10$example', 'admin') +ON CONFLICT (id) DO NOTHING; diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..d771ceb --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,45 @@ +# Многоэтапная сборка для Angular приложения +FROM node:18-alpine AS builder + +# Установка рабочей директории +WORKDIR /app + +# Копирование package файлов +COPY package*.json ./ + +# Установка зависимостей +RUN npm ci --only=production + +# Копирование исходного кода +COPY . . + +# Сборка приложения +RUN npm run build:prod + +# Финальный образ с nginx +FROM nginx:alpine + +# Копирование собранного приложения +COPY --from=builder /app/dist/erp-mvp-frontend /usr/share/nginx/html + +# Копирование конфигурации nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Создание пользователя для безопасности +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Смена владельца файлов +RUN chown -R appuser:appgroup /usr/share/nginx/html && \ + chown -R appuser:appgroup /var/cache/nginx && \ + chown -R appuser:appgroup /var/log/nginx && \ + chown -R appuser:appgroup /etc/nginx/conf.d + +# Переключение на непривилегированного пользователя +USER appuser + +# Экспорт порта +EXPOSE 80 + +# Команда запуска +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..c867a0a --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,141 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "erp-mvp-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/erp-mvp-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/manifest.webmanifest" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ], + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "erp-mvp-frontend:build:production" + }, + "development": { + "browserTarget": "erp-mvp-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "erp-mvp-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], + "scripts": [] + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..55b3526 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,91 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Логирование + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # Основные настройки + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip сжатие + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Основной сервер + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Безопасность + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Кэширование статических файлов + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Angular routing - fallback на index.html + location / { + try_files $uri $uri/ /index.html; + } + + # API проксирование (опционально) + location /api/ { + proxy_pass http://core-service:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Document Service проксирование + location /doc-api/ { + proxy_pass http://doc-service:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Health check + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0fc8b6e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,67 @@ +{ + "name": "erp-mvp-frontend", + "version": "1.0.0", + "description": "ERP для мастеров - Angular PWA Frontend", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "build:prod": "ng build --configuration production", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e", + "pwa:build": "ng build --configuration production && ng add @angular/pwa" + }, + "dependencies": { + "@angular/animations": "^17.0.0", + "@angular/common": "^17.0.0", + "@angular/compiler": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/forms": "^17.0.0", + "@angular/platform-browser": "^17.0.0", + "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/router": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@angular/material": "^17.0.0", + "@angular/cdk": "^17.0.0", + "@zxing/ngx-scanner": "^3.0.0", + "@ngrx/store": "^17.0.0", + "@ngrx/effects": "^17.0.0", + "@ngrx/entity": "^17.0.0", + "@ngrx/store-devtools": "^17.0.0", + "rxjs": "^7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.2", + "tailwindcss": "^3.3.0", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.31" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.0.0", + "@angular/cli": "^17.0.0", + "@angular/compiler-cli": "^17.0.0", + "@angular/language-service": "^17.0.0", + "@types/jasmine": "~5.1.0", + "@types/node": "^18.0.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.2.2", + "eslint": "^8.0.0", + "@angular-eslint/eslint-plugin": "^17.0.0", + "@angular-eslint/eslint-plugin-template": "^17.0.0", + "@angular-eslint/template-parser": "^17.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts new file mode 100644 index 0000000..972c478 --- /dev/null +++ b/frontend/src/app/app.module.ts @@ -0,0 +1,100 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { ReactiveFormsModule } from '@angular/forms'; + +// Angular Material +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatListModule } from '@angular/material/list'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatBadgeModule } from '@angular/material/badge'; + +// NgRx +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; + +// QR Scanner +import { ZXingScannerModule } from '@zxing/ngx-scanner'; + +// Components +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { HeaderComponent } from './components/header/header.component'; +import { QRScannerComponent } from './components/qr-scanner/qr-scanner.component'; +import { LocationDetailsComponent } from './components/location-details/location-details.component'; +import { ItemListComponent } from './components/item-list/item-list.component'; +import { SearchComponent } from './components/search/search.component'; + +// Services +import { AuthInterceptor } from './services/auth.interceptor'; + +// Store +import { appReducers } from './store/app.reducers'; +import { AppEffects } from './store/app.effects'; + +// Environment +import { environment } from '../environments/environment'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + QRScannerComponent, + LocationDetailsComponent, + ItemListComponent, + SearchComponent + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + HttpClientModule, + ReactiveFormsModule, + AppRoutingModule, + + // Angular Material + MatToolbarModule, + MatButtonModule, + MatIconModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatSnackBarModule, + MatProgressSpinnerModule, + MatDialogModule, + MatListModule, + MatChipsModule, + MatBadgeModule, + + // QR Scanner + ZXingScannerModule, + + // NgRx + StoreModule.forRoot(appReducers), + EffectsModule.forRoot([AppEffects]), + StoreDevtoolsModule.instrument({ + maxAge: 25, + logOnly: environment.production + }) + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..dc82839 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/proto/core.proto b/proto/core.proto new file mode 100644 index 0000000..2f48969 --- /dev/null +++ b/proto/core.proto @@ -0,0 +1,197 @@ +syntax = "proto3"; + +package erp.core; + +option go_package = "erp-mvp/core-service/proto/core"; + +import "google/protobuf/timestamp.proto"; + +// Сервис для работы с местами хранения +service LocationService { + rpc GetLocation(GetLocationRequest) returns (LocationResponse); + rpc CreateLocation(CreateLocationRequest) returns (LocationResponse); + rpc UpdateLocation(UpdateLocationRequest) returns (LocationResponse); + rpc DeleteLocation(DeleteLocationRequest) returns (DeleteLocationResponse); + rpc ListLocations(ListLocationsRequest) returns (ListLocationsResponse); +} + +// Сервис для работы с товарами +service ItemService { + rpc GetItem(GetItemRequest) returns (ItemResponse); + rpc CreateItem(CreateItemRequest) returns (ItemResponse); + rpc UpdateItem(UpdateItemRequest) returns (ItemResponse); + rpc DeleteItem(DeleteItemRequest) returns (DeleteItemResponse); + rpc ListItems(ListItemsRequest) returns (ListItemsResponse); +} + +// Сервис для операций +service OperationService { + rpc PlaceItem(PlaceItemRequest) returns (OperationResponse); + rpc MoveItem(MoveItemRequest) returns (OperationResponse); + rpc SearchItems(SearchItemsRequest) returns (SearchItemsResponse); +} + +// Запросы для мест хранения +message GetLocationRequest { + string location_id = 1; + string organization_id = 2; +} + +message CreateLocationRequest { + string organization_id = 1; + string name = 2; + string address = 3; + string type = 4; + optional string parent_id = 5; + map coordinates = 6; +} + +message UpdateLocationRequest { + string location_id = 1; + string organization_id = 2; + optional string name = 3; + optional string address = 4; + optional string type = 5; + optional string parent_id = 6; + map coordinates = 7; +} + +message DeleteLocationRequest { + string location_id = 1; + string organization_id = 2; +} + +message ListLocationsRequest { + string organization_id = 1; + optional string parent_id = 2; + optional string type = 3; + int32 page = 4; + int32 page_size = 5; +} + +// Запросы для товаров +message GetItemRequest { + string item_id = 1; + string organization_id = 2; +} + +message CreateItemRequest { + string organization_id = 1; + string name = 2; + string description = 3; + string category = 4; +} + +message UpdateItemRequest { + string item_id = 1; + string organization_id = 2; + optional string name = 3; + optional string description = 4; + optional string category = 5; +} + +message DeleteItemRequest { + string item_id = 1; + string organization_id = 2; +} + +message ListItemsRequest { + string organization_id = 1; + optional string category = 2; + optional string search_query = 3; + int32 page = 4; + int32 page_size = 5; +} + +// Запросы для операций +message PlaceItemRequest { + string organization_id = 1; + string item_id = 2; + string location_id = 3; + int32 quantity = 4; +} + +message MoveItemRequest { + string organization_id = 1; + string item_id = 2; + string from_location_id = 3; + string to_location_id = 4; + int32 quantity = 5; +} + +message SearchItemsRequest { + string organization_id = 1; + string query = 2; + optional string category = 3; + optional string location_id = 4; + int32 page = 5; + int32 page_size = 6; +} + +// Ответы +message LocationResponse { + string id = 1; + string organization_id = 2; + optional string parent_id = 3; + string name = 4; + string address = 5; + string type = 6; + map coordinates = 7; + string qr_code = 8; + google.protobuf.Timestamp created_at = 9; + google.protobuf.Timestamp updated_at = 10; +} + +message ListLocationsResponse { + repeated LocationResponse locations = 1; + int32 total_count = 2; + int32 page = 3; + int32 page_size = 4; +} + +message DeleteLocationResponse { + bool success = 1; + string message = 2; +} + +message ItemResponse { + string id = 1; + string organization_id = 2; + string name = 3; + string description = 4; + string category = 5; + google.protobuf.Timestamp created_at = 6; + google.protobuf.Timestamp updated_at = 7; +} + +message ListItemsResponse { + repeated ItemResponse items = 1; + int32 total_count = 2; + int32 page = 3; + int32 page_size = 4; +} + +message DeleteItemResponse { + bool success = 1; + string message = 2; +} + +message OperationResponse { + bool success = 1; + string message = 2; + string operation_id = 3; + google.protobuf.Timestamp created_at = 4; +} + +message SearchItemsResponse { + repeated ItemWithLocation items = 1; + int32 total_count = 2; + int32 page = 3; + int32 page_size = 4; +} + +message ItemWithLocation { + ItemResponse item = 1; + LocationResponse location = 2; + int32 quantity = 3; +} diff --git a/proto/document.proto b/proto/document.proto new file mode 100644 index 0000000..0b8555c --- /dev/null +++ b/proto/document.proto @@ -0,0 +1,161 @@ +syntax = "proto3"; + +package erp.document; + +option go_package = "erp-mvp/core-service/proto/document"; + +import "google/protobuf/timestamp.proto"; + +// Сервис для генерации документов +service DocumentService { + rpc GenerateQRCode(QRCodeRequest) returns (QRCodeResponse); + rpc GenerateReport(ReportRequest) returns (ReportResponse); + rpc GetDocumentStatus(StatusRequest) returns (StatusResponse); + rpc GetDocumentDownload(DownloadRequest) returns (DownloadResponse); +} + +// Сервис для работы с шаблонами +service TemplateService { + rpc GetTemplate(TemplateRequest) returns (TemplateResponse); + rpc ListTemplates(ListTemplatesRequest) returns (ListTemplatesResponse); + rpc CreateTemplate(CreateTemplateRequest) returns (TemplateResponse); + rpc UpdateTemplate(UpdateTemplateRequest) returns (TemplateResponse); + rpc DeleteTemplate(DeleteTemplateRequest) returns (DeleteTemplateResponse); +} + +// Запросы для QR-кодов +message QRCodeRequest { + string location_id = 1; + string location_address = 2; + string organization_id = 3; + int32 size = 4; + int32 border = 5; +} + +message QRCodeResponse { + string document_id = 1; + string qr_code_url = 2; + string qr_code_data = 3; + google.protobuf.Timestamp expires_at = 4; +} + +// Запросы для отчетов +message ReportRequest { + string report_type = 1; + string organization_id = 2; + map filters = 3; + DocumentFormat format = 4; +} + +message ReportResponse { + string document_id = 1; + string download_url = 2; + int64 file_size = 3; + google.protobuf.Timestamp expires_at = 4; +} + +// Запросы для статуса документа +message StatusRequest { + string document_id = 1; +} + +message StatusResponse { + string document_id = 1; + DocumentStatus status = 2; + int32 progress = 3; + string error_message = 4; + google.protobuf.Timestamp created_at = 5; + google.protobuf.Timestamp updated_at = 6; +} + +// Запросы для скачивания +message DownloadRequest { + string document_id = 1; +} + +message DownloadResponse { + string document_id = 1; + string download_url = 2; + int64 file_size = 3; + string filename = 4; + DocumentFormat format = 5; +} + +// Запросы для шаблонов +message TemplateRequest { + string template_name = 1; +} + +message TemplateResponse { + string name = 1; + string description = 2; + repeated string variables = 3; + string content = 4; + google.protobuf.Timestamp created_at = 5; + google.protobuf.Timestamp updated_at = 6; +} + +message ListTemplatesRequest { + string category = 1; + int32 page = 2; + int32 page_size = 3; +} + +message ListTemplatesResponse { + repeated TemplateInfo templates = 1; + int32 total_count = 2; + int32 page = 3; + int32 page_size = 4; +} + +message CreateTemplateRequest { + string name = 1; + string description = 2; + string content = 3; + string category = 4; + repeated string variables = 5; +} + +message UpdateTemplateRequest { + string name = 1; + optional string description = 2; + optional string content = 3; + optional string category = 4; + repeated string variables = 5; +} + +message DeleteTemplateRequest { + string name = 1; +} + +message DeleteTemplateResponse { + bool success = 1; + string message = 2; +} + +// Информация о шаблоне +message TemplateInfo { + string name = 1; + string description = 2; + string category = 3; + repeated string variables = 4; + google.protobuf.Timestamp created_at = 5; + google.protobuf.Timestamp updated_at = 6; +} + +// Перечисления +enum DocumentFormat { + DOCUMENT_FORMAT_UNSPECIFIED = 0; + DOCUMENT_FORMAT_PDF = 1; + DOCUMENT_FORMAT_EXCEL = 2; + DOCUMENT_FORMAT_WORD = 3; + DOCUMENT_FORMAT_QR_CODE = 4; +} + +enum DocumentStatus { + DOCUMENT_STATUS_UNSPECIFIED = 0; + DOCUMENT_STATUS_PENDING = 1; + DOCUMENT_STATUS_PROCESSING = 2; + DOCUMENT_STATUS_COMPLETED = 3; + DOCUMENT_STATUS_FAILED = 4; +}