git init
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This directory defines the AI generation context.
|
||||||
|
|
||||||
|
All code generation must follow the rules described in these documents.
|
||||||
254
backend/architecture.md
Normal file
254
backend/architecture.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Backend Architecture
|
||||||
|
|
||||||
|
Backend stack:
|
||||||
|
|
||||||
|
- Node.js
|
||||||
|
- TypeScript
|
||||||
|
- NestJS
|
||||||
|
- Prisma ORM
|
||||||
|
- PostgreSQL
|
||||||
|
|
||||||
|
The backend is generated from the DSL specification.
|
||||||
|
|
||||||
|
Each DSL entity becomes:
|
||||||
|
|
||||||
|
- Prisma model
|
||||||
|
- NestJS module
|
||||||
|
- CRUD controller
|
||||||
|
- Service
|
||||||
|
- DTO definitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
server/
|
||||||
|
package.json
|
||||||
|
|
||||||
|
prisma/
|
||||||
|
schema.prisma
|
||||||
|
|
||||||
|
src/
|
||||||
|
main.ts
|
||||||
|
app.module.ts
|
||||||
|
|
||||||
|
modules/
|
||||||
|
|
||||||
|
{entity}/
|
||||||
|
{entity}.module.ts
|
||||||
|
{entity}.controller.ts
|
||||||
|
{entity}.service.ts
|
||||||
|
|
||||||
|
dto/
|
||||||
|
create-{entity}.dto.ts
|
||||||
|
update-{entity}.dto.ts
|
||||||
|
{entity}.response.dto.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Module Rules
|
||||||
|
|
||||||
|
Each entity generates exactly one NestJS module.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Entity:
|
||||||
|
Equipment
|
||||||
|
|
||||||
|
Module:
|
||||||
|
|
||||||
|
modules/
|
||||||
|
equipment/
|
||||||
|
equipment.module.ts
|
||||||
|
equipment.controller.ts
|
||||||
|
equipment.service.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Controller Rules
|
||||||
|
|
||||||
|
Each entity controller must expose these endpoints:
|
||||||
|
|
||||||
|
- GET /{resource} — list
|
||||||
|
- GET /{resource}/:pk — get one
|
||||||
|
- POST /{resource} — create
|
||||||
|
- PATCH /{resource}/:pk — update
|
||||||
|
- DELETE /{resource}/:pk — delete
|
||||||
|
|
||||||
|
The path parameter **:pk** must use the **primary key attribute name** from the DSL, not always `:id`.
|
||||||
|
|
||||||
|
## Path parameter by primary key
|
||||||
|
|
||||||
|
| Entity | Primary key (DSL) | Path parameter | Example routes |
|
||||||
|
| ------------- | ----------------- | -------------- | -------------------------------------------------------- |
|
||||||
|
| Equipment | id | :id | GET /equipment/:id, PATCH /equipment/:id |
|
||||||
|
| EquipmentType | code | :code | GET /equipment-types/:code, PATCH /equipment-types/:code |
|
||||||
|
| RepairOrder | id | :id | GET /repair-orders/:id, PATCH /repair-orders/:id |
|
||||||
|
|
||||||
|
**Rule:** Use the actual primary key name. For example, **EquipmentType** has PK `code`, so routes must be:
|
||||||
|
|
||||||
|
- GET /equipment-types/:code
|
||||||
|
- PATCH /equipment-types/:code
|
||||||
|
- DELETE /equipment-types/:code
|
||||||
|
|
||||||
|
**Do not** use `/equipment-types/:id` when the entity primary key is `code`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Health Endpoint
|
||||||
|
|
||||||
|
Every generated backend must expose a **health endpoint** so that runtime and orchestration can verify the API is up.
|
||||||
|
|
||||||
|
- **Path:** `GET /health`
|
||||||
|
- **Response:** JSON with a status indicator (e.g. `{ "status": "ok" }`).
|
||||||
|
|
||||||
|
## Controller example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Controller("health")
|
||||||
|
export class HealthController {
|
||||||
|
@Get()
|
||||||
|
getHealth() {
|
||||||
|
return { status: "ok" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the health controller in the root app module (or a dedicated health module). No authentication required for this endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# List Endpoint
|
||||||
|
|
||||||
|
List endpoint must support pagination and filters via query parameters.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
GET /equipment?page=0&size=10
|
||||||
|
|
||||||
|
Response format must follow React Admin requirements:
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": [],
|
||||||
|
"total": number
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Service Layer
|
||||||
|
|
||||||
|
Services implement CRUD operations using Prisma.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
findAll()
|
||||||
|
findOne(id)
|
||||||
|
create(data)
|
||||||
|
update(id, data)
|
||||||
|
remove(id)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DTO Rules
|
||||||
|
|
||||||
|
Create DTO:
|
||||||
|
|
||||||
|
- contains required fields
|
||||||
|
- does NOT contain generated primary keys
|
||||||
|
|
||||||
|
Update DTO:
|
||||||
|
|
||||||
|
- all fields optional
|
||||||
|
|
||||||
|
Response DTO:
|
||||||
|
|
||||||
|
- mirrors domain entity attributes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Naming Conventions
|
||||||
|
|
||||||
|
## Entity naming
|
||||||
|
|
||||||
|
DSL entities use PascalCase. Generated backend artifacts use the same base name in lowercase for folders and file prefixes.
|
||||||
|
|
||||||
|
- **Equipment** → equipment module
|
||||||
|
- **EquipmentType** → equipment-type module (or equipment-type as path segment)
|
||||||
|
- **RepairOrder** → repair-order module
|
||||||
|
|
||||||
|
## Module naming
|
||||||
|
|
||||||
|
One entity = one module folder. Folder name = entity name in kebab-case (lowercase, hyphen-separated).
|
||||||
|
|
||||||
|
- Equipment → `modules/equipment/`
|
||||||
|
- EquipmentType → `modules/equipment-type/`
|
||||||
|
- RepairOrder → `modules/repair-order/`
|
||||||
|
|
||||||
|
## Controller naming
|
||||||
|
|
||||||
|
- File: `{entity-kebab}.controller.ts`
|
||||||
|
- Class: `EquipmentController`, `EquipmentTypeController`, `RepairOrderController`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `equipment.controller.ts`
|
||||||
|
- `equipment-type.controller.ts`
|
||||||
|
- `repair-order.controller.ts`
|
||||||
|
|
||||||
|
## Service naming
|
||||||
|
|
||||||
|
- File: `{entity-kebab}.service.ts`
|
||||||
|
- Class: `EquipmentService`, `EquipmentTypeService`, `RepairOrderService`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `equipment.service.ts`
|
||||||
|
- `equipment-type.service.ts`
|
||||||
|
- `repair-order.service.ts`
|
||||||
|
|
||||||
|
## DTO naming
|
||||||
|
|
||||||
|
- Create: `create-{entity-kebab}.dto.ts` (e.g. `create-equipment.dto.ts`, `create-repair-order.dto.ts`)
|
||||||
|
- Update: `update-{entity-kebab}.dto.ts` (e.g. `update-equipment.dto.ts`)
|
||||||
|
- Response: `{entity-kebab}.response.dto.ts` or use entity name for list/detail response types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource Naming Rules
|
||||||
|
|
||||||
|
API resource paths are derived from the entity name:
|
||||||
|
|
||||||
|
1. **PascalCase → kebab-case:** Replace camelCase with lowercase hyphenated segments.
|
||||||
|
2. **Pluralize:** Use plural form for the resource path (list endpoint represents a collection).
|
||||||
|
|
||||||
|
| Entity (DSL) | API path (resource) |
|
||||||
|
| ------------- | ------------------- |
|
||||||
|
| Equipment | /equipment |
|
||||||
|
| EquipmentType | /equipment-types |
|
||||||
|
| RepairOrder | /repair-orders |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- **Equipment** → `equipment` (already singular-looking; path is still `/equipment` for consistency with REST resource naming).
|
||||||
|
- **EquipmentType** → `equipment-types` (camelCase "EquipmentType" → "equipment-type", then plural → "equipment-types").
|
||||||
|
- **RepairOrder** → `repair-orders` ("RepairOrder" → "repair-order" → "repair-orders").
|
||||||
|
|
||||||
|
Standard endpoints per resource:
|
||||||
|
|
||||||
|
- GET /{resource} — list
|
||||||
|
- GET /{resource}/:pk — get one (pk = primary key name, e.g. :id or :code)
|
||||||
|
- POST /{resource} — create
|
||||||
|
- PATCH /{resource}/:pk — update
|
||||||
|
- DELETE /{resource}/:pk — delete
|
||||||
|
|
||||||
|
See **Controller Rules** above for the rule that :pk must match the entity's primary key attribute name.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Environment and runtime
|
||||||
|
|
||||||
|
- **Environment variables:** Backend requires at least `DATABASE_URL`. See **backend/runtime-rules.md**.
|
||||||
|
- **.env:** Generated project must include a `.env` (and `.env.example`) with `DATABASE_URL` so the app starts without runtime errors.
|
||||||
|
- **PrismaService:** Must follow **backend/prisma-service.md** (OnModuleInit, $connect; no beforeExit).
|
||||||
|
- **Prisma client:** Add `"postinstall": "prisma generate"` (or equivalent) to package.json so the client is generated after install.
|
||||||
|
- **Migrations:** Document or run `npx prisma migrate dev` after schema generation. See **backend/runtime-rules.md** and **generation/backend-generation.md**.
|
||||||
70
backend/database-runtime.md
Normal file
70
backend/database-runtime.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Database Runtime
|
||||||
|
|
||||||
|
The generated project must include a **PostgreSQL development database** so the application can run immediately after generation without manual database setup.
|
||||||
|
|
||||||
|
Use **Docker** to provision the database.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# docker-compose.yml
|
||||||
|
|
||||||
|
The generator must create a `docker-compose.yml` file at the **project root** (same level as `server/` and `client/` directories).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: toir-postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: toir
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
|
||||||
|
- **Location:** Project root (e.g. `TOiR-generation/docker-compose.yml` or monorepo root).
|
||||||
|
- **Service name:** Can be `postgres` or project-specific (e.g. `toir-postgres` as container_name).
|
||||||
|
- **Credentials and DB name** must match the `DATABASE_URL` in `server/.env`:
|
||||||
|
- User: `postgres`
|
||||||
|
- Password: `postgres`
|
||||||
|
- Database: `toir`
|
||||||
|
- Host: `localhost`
|
||||||
|
- Port: `5432`
|
||||||
|
- **Volume:** Use a named volume so data persists across container restarts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Start the database before running the backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
Without this file and a running database, the backend will fail at runtime with errors such as:
|
||||||
|
|
||||||
|
```
|
||||||
|
PrismaClientInitializationError P1001: Can't reach database server at localhost:5432
|
||||||
|
```
|
||||||
166
backend/prisma-rules.md
Normal file
166
backend/prisma-rules.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# DSL → Prisma Mapping Rules
|
||||||
|
|
||||||
|
The domain DSL defines the database schema.
|
||||||
|
|
||||||
|
Each DSL entity becomes a Prisma model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Type Mapping (Prisma schema)
|
||||||
|
|
||||||
|
| DSL Type | Prisma Type |
|
||||||
|
|--------|-------------|
|
||||||
|
| string | String |
|
||||||
|
| uuid | String @id @default(uuid()) |
|
||||||
|
| integer | Int |
|
||||||
|
| decimal | Decimal |
|
||||||
|
| date | DateTime |
|
||||||
|
| text | String |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DTO Type Mapping (API / serialization)
|
||||||
|
|
||||||
|
For DTOs and API responses, use types that serialize to JSON without errors. Prisma's `Decimal` and `DateTime` do not always serialize predictably; map them as follows in generated DTOs:
|
||||||
|
|
||||||
|
| DSL Type | Prisma Type | DTO / API type | Notes |
|
||||||
|
|----------|-------------|----------------|-------|
|
||||||
|
| string | String | string | — |
|
||||||
|
| uuid | String | string | — |
|
||||||
|
| integer | Int | number | — |
|
||||||
|
| decimal | Decimal | **string** | Avoid Prisma Decimal in DTO; use string to prevent serialization issues. |
|
||||||
|
| date | DateTime | **string** | ISO 8601 date string (e.g. `"2025-03-08T00:00:00.000Z"`). |
|
||||||
|
| text | String | string | — |
|
||||||
|
| enum | enum | string | Enum value as string. |
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- **decimal** → In create/update/response DTOs, use `string` (or a type that serializes to string). Convert to/from Prisma `Decimal` only in the service layer.
|
||||||
|
- **date** → In DTOs, use `string` (ISO date). Convert to/from Prisma `DateTime` in the service layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Enum Mapping
|
||||||
|
|
||||||
|
DSL enum becomes Prisma enum.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
DSL
|
||||||
|
|
||||||
|
enum EquipmentStatus {
|
||||||
|
Active
|
||||||
|
Repair
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma
|
||||||
|
|
||||||
|
enum EquipmentStatus {
|
||||||
|
Active
|
||||||
|
Repair
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Foreign Keys
|
||||||
|
|
||||||
|
DSL foreign keys become Prisma relations.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
DSL
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
key foreign {
|
||||||
|
relates EquipmentType.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma
|
||||||
|
|
||||||
|
equipmentTypeCode String
|
||||||
|
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Primary Keys
|
||||||
|
|
||||||
|
Primary keys defined in DSL must become Prisma model identifiers.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
key primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma
|
||||||
|
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prisma Service Rules
|
||||||
|
|
||||||
|
Use Prisma v5 compatible service. See **backend/prisma-service.md** for full specification.
|
||||||
|
|
||||||
|
- PrismaService MUST implement `OnModuleInit` and call `await this.$connect()` in `onModuleInit()`.
|
||||||
|
- PrismaService MUST NOT use the deprecated `this.$on('beforeExit', ...)` hook (not supported in Prisma 5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prisma Lifecycle Rules
|
||||||
|
|
||||||
|
These lifecycle rules must be applied in generated projects so Prisma runtime is reliable after generation and after dependency installation.
|
||||||
|
|
||||||
|
## 1) Generate Prisma client after schema changes
|
||||||
|
|
||||||
|
Whenever `prisma/schema.prisma` is generated or updated, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) postinstall lifecycle
|
||||||
|
|
||||||
|
Generated `server/package.json` must include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "prisma generate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures Prisma client generation after `npm install`.
|
||||||
|
|
||||||
|
## 3) Migration lifecycle
|
||||||
|
|
||||||
|
Use development migration workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this after database startup and before starting the backend server.
|
||||||
|
|
||||||
|
## 4) Seed lifecycle
|
||||||
|
|
||||||
|
If seed is configured (see `backend/seed-rules.md`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated `server/package.json` should include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `tsx prisma/seed.ts` if the project standard uses `tsx` instead of `ts-node`.
|
||||||
80
backend/prisma-service.md
Normal file
80
backend/prisma-service.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# PrismaService Implementation
|
||||||
|
|
||||||
|
The generated backend must provide a NestJS injectable service that wraps PrismaClient. This document defines the **only** supported implementation for Prisma 5+.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Correct Implementation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why this pattern
|
||||||
|
|
||||||
|
- **OnModuleInit:** NestJS lifecycle hook; `onModuleInit()` runs when the module is initialized, ensuring the database connection is established before handling requests.
|
||||||
|
- **$connect():** Explicitly connects the Prisma client. Required for reliable connection handling in serverless or long-running apps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deprecated: Do NOT Use
|
||||||
|
|
||||||
|
The following pattern is **deprecated** in Prisma 5 and must **not** be generated:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG — do not generate
|
||||||
|
this.$on("beforeExit", async () => {
|
||||||
|
await this.$disconnect();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- `beforeExit` is not supported in Prisma 5.
|
||||||
|
- Using it will cause runtime errors or warnings.
|
||||||
|
|
||||||
|
## Rule for generators
|
||||||
|
|
||||||
|
Generated `PrismaService` (or equivalent) must:
|
||||||
|
|
||||||
|
1. Extend `PrismaClient`.
|
||||||
|
2. Implement `OnModuleInit`.
|
||||||
|
3. Call `await this.$connect()` in `onModuleInit()`.
|
||||||
|
4. **Not** use `this.$on('beforeExit', ...)` or any `beforeExit` hook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Module Registration
|
||||||
|
|
||||||
|
Register the service in a dedicated Prisma module (or in `AppModule`) so it can be injected into other services:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// prisma.module.ts (or app.module.ts)
|
||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `@Global()` allows injecting `PrismaService` in any module without re-importing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# File location
|
||||||
|
|
||||||
|
Generated file:
|
||||||
|
|
||||||
|
- `src/prisma.prisma.service.ts` or `src/prisma.service.ts`
|
||||||
|
|
||||||
|
Class name: `PrismaService`.
|
||||||
94
backend/runtime-rules.md
Normal file
94
backend/runtime-rules.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Backend Runtime Rules
|
||||||
|
|
||||||
|
This document defines runtime configuration requirements for the generated backend. Generators must produce a project that runs without errors when these rules are followed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
The backend **must** have a `.env` file at the project root (e.g. `server/.env` or backend package root). This file must **not** be committed with real credentials; provide an example instead (e.g. `.env.example`).
|
||||||
|
|
||||||
|
## Required variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
| ------------ | -------- | --------------------------------------- |
|
||||||
|
| DATABASE_URL | Yes | PostgreSQL connection string for Prisma |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generation requirement
|
||||||
|
|
||||||
|
When generating the backend, **always** create:
|
||||||
|
|
||||||
|
1. **`.env.example`** — with placeholder DATABASE_URL and instructions.
|
||||||
|
2. **`.env`** — with the same placeholder so the app can start; user replaces with real values.
|
||||||
|
|
||||||
|
If the generator does not create `.env`, the first run will fail with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Environment variable not found: DATABASE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prisma Client Generation
|
||||||
|
|
||||||
|
After the Prisma schema is generated or modified, the Prisma client must be generated.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle rule
|
||||||
|
|
||||||
|
Add to generated `package.json` so that `prisma generate` runs after every `npm install`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "prisma generate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that after cloning or installing dependencies, the Prisma client is available without a manual step.
|
||||||
|
|
||||||
|
**Note:** Path may need to be adjusted if Prisma schema lives in a subfolder (e.g. `prisma generate` is typically run from the package root where `prisma/schema.prisma` exists).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Migration
|
||||||
|
|
||||||
|
The generation process must document and support database migration.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Run after:
|
||||||
|
|
||||||
|
1. Prisma schema has been generated or updated.
|
||||||
|
2. `npx prisma generate` has been run.
|
||||||
|
|
||||||
|
## Generation requirement
|
||||||
|
|
||||||
|
1. Include migration in the backend generation pipeline (see `generation/backend-generation.md`).
|
||||||
|
2. Document in README or post-generation validation that the user must run `npx prisma migrate dev` before first run (or provide a setup script that runs it).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
| Requirement | Action |
|
||||||
|
| --------------- | ------------------------------------------------------------- |
|
||||||
|
| DATABASE_URL | Create `.env` and `.env.example` with DATABASE_URL |
|
||||||
|
| Prisma client | Run `npx prisma generate`; add `postinstall` script |
|
||||||
|
| Database schema | Document/run `npx prisma migrate dev` after schema generation |
|
||||||
82
backend/seed-rules.md
Normal file
82
backend/seed-rules.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Seed Data Rules
|
||||||
|
|
||||||
|
The generator must create a **Prisma seed script** so the development database contains minimal sample data. This allows the frontend and API to be used immediately after migration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Seed Script Location
|
||||||
|
|
||||||
|
**File:** `server/prisma/seed.ts`
|
||||||
|
|
||||||
|
(Or `server/prisma/seed.js` if the project uses JavaScript; TypeScript is preferred and may require `ts-node` or `tsx` to run.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Seed Data Requirements
|
||||||
|
|
||||||
|
The seed script must create **minimal sample data** for at least one record per main entity, so that:
|
||||||
|
|
||||||
|
- List views show data.
|
||||||
|
- Reference fields (e.g. equipment type, equipment) have valid options.
|
||||||
|
- The app is demo-ready without manual data entry.
|
||||||
|
|
||||||
|
## Example scope (TOiR-style domain)
|
||||||
|
|
||||||
|
- **One EquipmentType** (e.g. code `"pump"`, name `"Pump"`).
|
||||||
|
- **One Equipment** (e.g. linked to that type, with required fields filled).
|
||||||
|
- **One RepairOrder** (e.g. linked to that equipment, with required fields filled).
|
||||||
|
|
||||||
|
Order matters: create EquipmentType first, then Equipment (references type), then RepairOrder (references equipment). Use Prisma `create` with the generated client; respect unique constraints and foreign keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prisma Seed Configuration
|
||||||
|
|
||||||
|
The generator must add the following to **`server/package.json`**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the project uses a different runner (e.g. `tsx`), use that instead:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure the seed runner is installed (e.g. `ts-node` as dev dependency) so that:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
runs successfully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Running the Seed
|
||||||
|
|
||||||
|
After migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
Or document that the user can run `npx prisma db seed` once after the first migration to populate sample data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
| Requirement | Action |
|
||||||
|
|--------------------|--------|
|
||||||
|
| Seed script | Create `server/prisma/seed.ts` (or equivalent). |
|
||||||
|
| Sample data | At least one EquipmentType, one Equipment, one RepairOrder (or equivalent for the DSL). |
|
||||||
|
| package.json | Add `"prisma": { "seed": "ts-node prisma/seed.ts" }` (or tsx). |
|
||||||
|
| Seed runner | Ensure ts-node (or tsx) is available so `prisma db seed` works. |
|
||||||
97
backend/service-rules.md
Normal file
97
backend/service-rules.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Service Layer Rules
|
||||||
|
|
||||||
|
Generated NestJS services must follow these rules so that update operations work correctly with React Admin and Prisma.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Update Payload Sanitization
|
||||||
|
|
||||||
|
React Admin (and many REST clients) **always send `id`** in PATCH/PUT request bodies.
|
||||||
|
|
||||||
|
Example payload from React Admin:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "003",
|
||||||
|
"name": "Pump"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Some entities use a **different primary key** (e.g. `code`). The API response includes `id` (mapped from the PK) for React Admin compatibility, but the Prisma model may have no `id` field—only `code`. If the service passes the incoming DTO directly to Prisma:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG — causes runtime error
|
||||||
|
prisma.equipmentType.update({
|
||||||
|
where: { code },
|
||||||
|
data: dto // dto contains "id", which is not a Prisma field
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Prisma throws because `id` (and possibly other non-updatable fields) are not on the model or must not be written in `data`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
|
||||||
|
1. **Update payload must be sanitized** before passing to the ORM. Do not pass the raw request body as `data` to `prisma.*.update()`.
|
||||||
|
|
||||||
|
2. **Remove `id`** from the update payload. React Admin sends `id` for identity; it must not be written to the database as a column (unless the entity actually has an `id` column and it is intended to be immutable on update).
|
||||||
|
|
||||||
|
3. **Remove the primary key** field from the update payload. The primary key is used in `where`; it must not appear in `data`. For example, remove `code` from `data` when updating by `code`.
|
||||||
|
|
||||||
|
4. **Remove readonly attributes** (e.g. created timestamps, server-generated fields) if they are present in the DTO, so they are not passed to Prisma `data`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Example Implementation
|
||||||
|
|
||||||
|
Destructure identity and primary key (and any other non-updatable fields) out of the DTO, then pass only the rest as `data`:
|
||||||
|
|
||||||
|
**Entity with primary key `code` (e.g. EquipmentType):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
update(code: string, dto: UpdateEquipmentTypeDto) {
|
||||||
|
const { id, code: _pk, ...data } = dto as any;
|
||||||
|
return this.prisma.equipmentType.update({
|
||||||
|
where: { code },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if the DTO type does not include `id` or `code`, explicitly omit only the fields that must not be written:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
update(code: string, dto: UpdateEquipmentTypeDto) {
|
||||||
|
const { id, code: _pk, ...data } = dto as Record<string, unknown>;
|
||||||
|
return this.prisma.equipmentType.update({
|
||||||
|
where: { code },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entity with primary key `id` (e.g. Equipment):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
update(id: string, dto: UpdateEquipmentDto) {
|
||||||
|
const { id: _pk, ...data } = dto as any;
|
||||||
|
return this.prisma.equipment.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This way, `id` (and the PK) are never passed into `data`, and Prisma does not receive unknown or read-only fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
| Rule | Action |
|
||||||
|
|------|--------|
|
||||||
|
| Sanitize update payload | Before `prisma.*.update()`, strip non-data fields from the DTO. |
|
||||||
|
| Remove `id` | Do not pass `id` in `data` unless the entity has an updatable `id` (rare). |
|
||||||
|
| Remove primary key | Use PK only in `where`; omit from `data`. |
|
||||||
|
| Remove readonly fields | Omit created_at, server-only fields, etc. from `data`. |
|
||||||
18
client/.eslintrc.cjs
Normal file
18
client/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
34
client/.gitignore
vendored
Normal file
34
client/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
30
client/README.md
Normal file
30
client/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
// other rules...
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||||
13
client/index.html
Normal file
13
client/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4871
client/package-lock.json
generated
Normal file
4871
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
client/package.json
Normal file
33
client/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/material": "^7.3.9",
|
||||||
|
"ra-data-simple-rest": "^5.14.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-admin": "^5.14.4",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.55",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
client/public/vite.svg
Normal file
1
client/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
48
client/src/App.tsx
Normal file
48
client/src/App.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Admin, Resource } from 'react-admin';
|
||||||
|
import dataProvider from './dataProvider';
|
||||||
|
|
||||||
|
import { EquipmentTypeList } from './resources/equipment-type/EquipmentTypeList';
|
||||||
|
import { EquipmentTypeCreate } from './resources/equipment-type/EquipmentTypeCreate';
|
||||||
|
import { EquipmentTypeEdit } from './resources/equipment-type/EquipmentTypeEdit';
|
||||||
|
import { EquipmentTypeShow } from './resources/equipment-type/EquipmentTypeShow';
|
||||||
|
|
||||||
|
import { EquipmentList } from './resources/equipment/EquipmentList';
|
||||||
|
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
|
||||||
|
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
|
||||||
|
import { EquipmentShow } from './resources/equipment/EquipmentShow';
|
||||||
|
|
||||||
|
import { RepairOrderList } from './resources/repair-order/RepairOrderList';
|
||||||
|
import { RepairOrderCreate } from './resources/repair-order/RepairOrderCreate';
|
||||||
|
import { RepairOrderEdit } from './resources/repair-order/RepairOrderEdit';
|
||||||
|
import { RepairOrderShow } from './resources/repair-order/RepairOrderShow';
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<Admin dataProvider={dataProvider}>
|
||||||
|
<Resource
|
||||||
|
name="equipment-types"
|
||||||
|
options={{ label: 'Виды оборудования' }}
|
||||||
|
list={EquipmentTypeList}
|
||||||
|
create={EquipmentTypeCreate}
|
||||||
|
edit={EquipmentTypeEdit}
|
||||||
|
show={EquipmentTypeShow}
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
name="equipment"
|
||||||
|
options={{ label: 'Оборудование' }}
|
||||||
|
list={EquipmentList}
|
||||||
|
create={EquipmentCreate}
|
||||||
|
edit={EquipmentEdit}
|
||||||
|
show={EquipmentShow}
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
name="repair-orders"
|
||||||
|
options={{ label: 'Заявки на ремонт' }}
|
||||||
|
list={RepairOrderList}
|
||||||
|
create={RepairOrderCreate}
|
||||||
|
edit={RepairOrderEdit}
|
||||||
|
show={RepairOrderShow}
|
||||||
|
/>
|
||||||
|
</Admin>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
client/src/assets/react.svg
Normal file
1
client/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
125
client/src/dataProvider.ts
Normal file
125
client/src/dataProvider.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { DataProvider, fetchUtils } from 'react-admin';
|
||||||
|
|
||||||
|
const apiUrl = 'http://localhost:3000';
|
||||||
|
const httpClient = fetchUtils.fetchJson;
|
||||||
|
|
||||||
|
const dataProvider: DataProvider = {
|
||||||
|
getList: async (resource, params) => {
|
||||||
|
const { page, perPage } = params.pagination!;
|
||||||
|
const { field, order } = params.sort!;
|
||||||
|
const start = (page - 1) * perPage;
|
||||||
|
const end = page * perPage;
|
||||||
|
|
||||||
|
const query: Record<string, string> = {
|
||||||
|
_start: String(start),
|
||||||
|
_end: String(end),
|
||||||
|
_sort: field,
|
||||||
|
_order: order,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.filter) {
|
||||||
|
Object.keys(params.filter).forEach((key) => {
|
||||||
|
const val = params.filter[key];
|
||||||
|
if (val !== undefined && val !== null && val !== '') {
|
||||||
|
query[key] = String(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = new URLSearchParams(query).toString();
|
||||||
|
const url = `${apiUrl}/${resource}?${queryString}`;
|
||||||
|
const { json, headers } = await httpClient(url);
|
||||||
|
|
||||||
|
const contentRange = headers.get('Content-Range');
|
||||||
|
const total = contentRange
|
||||||
|
? parseInt(contentRange.split('/').pop() || '0', 10)
|
||||||
|
: json.length;
|
||||||
|
|
||||||
|
return { data: json, total };
|
||||||
|
},
|
||||||
|
|
||||||
|
getOne: async (resource, params) => {
|
||||||
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`);
|
||||||
|
return { data: json };
|
||||||
|
},
|
||||||
|
|
||||||
|
getMany: async (resource, params) => {
|
||||||
|
const query = params.ids.map((id) => `id=${id}`).join('&');
|
||||||
|
const { json } = await httpClient(`${apiUrl}/${resource}?${query}`);
|
||||||
|
return { data: json };
|
||||||
|
},
|
||||||
|
|
||||||
|
getManyReference: async (resource, params) => {
|
||||||
|
const { page, perPage } = params.pagination!;
|
||||||
|
const { field, order } = params.sort!;
|
||||||
|
const start = (page - 1) * perPage;
|
||||||
|
const end = page * perPage;
|
||||||
|
|
||||||
|
const query: Record<string, string> = {
|
||||||
|
_start: String(start),
|
||||||
|
_end: String(end),
|
||||||
|
_sort: field,
|
||||||
|
_order: order,
|
||||||
|
[params.target]: String(params.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = new URLSearchParams(query).toString();
|
||||||
|
const url = `${apiUrl}/${resource}?${queryString}`;
|
||||||
|
const { json, headers } = await httpClient(url);
|
||||||
|
|
||||||
|
const contentRange = headers.get('Content-Range');
|
||||||
|
const total = contentRange
|
||||||
|
? parseInt(contentRange.split('/').pop() || '0', 10)
|
||||||
|
: json.length;
|
||||||
|
|
||||||
|
return { data: json, total };
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (resource, params) => {
|
||||||
|
const { json } = await httpClient(`${apiUrl}/${resource}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params.data),
|
||||||
|
});
|
||||||
|
return { data: json };
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (resource, params) => {
|
||||||
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(params.data),
|
||||||
|
});
|
||||||
|
return { data: json };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMany: async (resource, params) => {
|
||||||
|
const responses = await Promise.all(
|
||||||
|
params.ids.map((id) =>
|
||||||
|
httpClient(`${apiUrl}/${resource}/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(params.data),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { data: responses.map(({ json }) => json.id) };
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (resource, params) => {
|
||||||
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return { data: json };
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMany: async (resource, params) => {
|
||||||
|
const responses = await Promise.all(
|
||||||
|
params.ids.map((id) =>
|
||||||
|
httpClient(`${apiUrl}/${resource}/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { data: responses.map(({ json }) => json.id) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dataProvider;
|
||||||
9
client/src/main.tsx
Normal file
9
client/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
13
client/src/resources/equipment-type/EquipmentTypeCreate.tsx
Normal file
13
client/src/resources/equipment-type/EquipmentTypeCreate.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Create, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
||||||
|
|
||||||
|
export const EquipmentTypeCreate = () => (
|
||||||
|
<Create>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="code" label="Код" isRequired />
|
||||||
|
<TextInput source="name" label="Наименование" isRequired />
|
||||||
|
<TextInput source="manufacturer" label="Производитель" />
|
||||||
|
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
||||||
|
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
13
client/src/resources/equipment-type/EquipmentTypeEdit.tsx
Normal file
13
client/src/resources/equipment-type/EquipmentTypeEdit.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Edit, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
||||||
|
|
||||||
|
export const EquipmentTypeEdit = () => (
|
||||||
|
<Edit>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="code" label="Код" disabled />
|
||||||
|
<TextInput source="name" label="Наименование" isRequired />
|
||||||
|
<TextInput source="manufacturer" label="Производитель" />
|
||||||
|
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
||||||
|
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
13
client/src/resources/equipment-type/EquipmentTypeList.tsx
Normal file
13
client/src/resources/equipment-type/EquipmentTypeList.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { List, Datagrid, TextField, NumberField } from 'react-admin';
|
||||||
|
|
||||||
|
export const EquipmentTypeList = () => (
|
||||||
|
<List>
|
||||||
|
<Datagrid rowClick="show">
|
||||||
|
<TextField source="code" label="Код" />
|
||||||
|
<TextField source="name" label="Наименование" />
|
||||||
|
<TextField source="manufacturer" label="Производитель" />
|
||||||
|
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
||||||
|
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
13
client/src/resources/equipment-type/EquipmentTypeShow.tsx
Normal file
13
client/src/resources/equipment-type/EquipmentTypeShow.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Show, SimpleShowLayout, TextField, NumberField } from 'react-admin';
|
||||||
|
|
||||||
|
export const EquipmentTypeShow = () => (
|
||||||
|
<Show>
|
||||||
|
<SimpleShowLayout>
|
||||||
|
<TextField source="code" label="Код" />
|
||||||
|
<TextField source="name" label="Наименование" />
|
||||||
|
<TextField source="manufacturer" label="Производитель" />
|
||||||
|
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
||||||
|
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
||||||
|
</SimpleShowLayout>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
36
client/src/resources/equipment/EquipmentCreate.tsx
Normal file
36
client/src/resources/equipment/EquipmentCreate.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Create,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
DateInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EquipmentCreate = () => (
|
||||||
|
<Create>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
|
<TextInput source="serialNumber" label="Заводской номер" />
|
||||||
|
<TextInput source="name" label="Наименование" isRequired />
|
||||||
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
||||||
|
<SelectInput optionText="name" optionValue="code" isRequired />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Active" />
|
||||||
|
<TextInput source="location" label="Место эксплуатации" />
|
||||||
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
|
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
|
||||||
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
||||||
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
|
<TextInput source="notes" label="Примечания" multiline />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
36
client/src/resources/equipment/EquipmentEdit.tsx
Normal file
36
client/src/resources/equipment/EquipmentEdit.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
DateInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EquipmentEdit = () => (
|
||||||
|
<Edit>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
|
<TextInput source="serialNumber" label="Заводской номер" />
|
||||||
|
<TextInput source="name" label="Наименование" isRequired />
|
||||||
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
||||||
|
<SelectInput optionText="name" optionValue="code" isRequired />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<TextInput source="location" label="Место эксплуатации" />
|
||||||
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
|
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
|
||||||
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
||||||
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
|
<TextInput source="notes" label="Примечания" multiline />
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
30
client/src/resources/equipment/EquipmentList.tsx
Normal file
30
client/src/resources/equipment/EquipmentList.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
List,
|
||||||
|
Datagrid,
|
||||||
|
TextField,
|
||||||
|
NumberField,
|
||||||
|
SelectField,
|
||||||
|
ReferenceField,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EquipmentList = () => (
|
||||||
|
<List>
|
||||||
|
<Datagrid rowClick="show">
|
||||||
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
|
<TextField source="name" label="Наименование" />
|
||||||
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
|
<TextField source="name" />
|
||||||
|
</ReferenceField>
|
||||||
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<TextField source="location" label="Место эксплуатации" />
|
||||||
|
<NumberField source="totalEngineHours" label="Наработка (ч)" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
36
client/src/resources/equipment/EquipmentShow.tsx
Normal file
36
client/src/resources/equipment/EquipmentShow.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Show,
|
||||||
|
SimpleShowLayout,
|
||||||
|
TextField,
|
||||||
|
NumberField,
|
||||||
|
DateField,
|
||||||
|
SelectField,
|
||||||
|
ReferenceField,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EquipmentShow = () => (
|
||||||
|
<Show>
|
||||||
|
<SimpleShowLayout>
|
||||||
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
|
<TextField source="serialNumber" label="Заводской номер" />
|
||||||
|
<TextField source="name" label="Наименование" />
|
||||||
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
|
<TextField source="name" />
|
||||||
|
</ReferenceField>
|
||||||
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<TextField source="location" label="Место эксплуатации" />
|
||||||
|
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
|
<NumberField source="totalEngineHours" label="Общая наработка (ч)" />
|
||||||
|
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
||||||
|
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
|
<TextField source="notes" label="Примечания" />
|
||||||
|
</SimpleShowLayout>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
46
client/src/resources/repair-order/RepairOrderCreate.tsx
Normal file
46
client/src/resources/repair-order/RepairOrderCreate.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Create,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
DateInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const repairKindChoices = [
|
||||||
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
|
{ id: 'TR', name: 'Текущий ремонт' },
|
||||||
|
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
|
||||||
|
{ id: 'KR', name: 'Капитальный ремонт' },
|
||||||
|
{ id: 'AR', name: 'Аварийный ремонт' },
|
||||||
|
{ id: 'MP', name: 'Метрологическая поверка' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Draft', name: 'Черновик' },
|
||||||
|
{ id: 'Approved', name: 'Утверждена' },
|
||||||
|
{ id: 'InWork', name: 'В работе' },
|
||||||
|
{ id: 'Done', name: 'Выполнена' },
|
||||||
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RepairOrderCreate = () => (
|
||||||
|
<Create>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="number" label="Номер заявки" isRequired />
|
||||||
|
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
|
||||||
|
<SelectInput optionText="name" isRequired />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Draft" />
|
||||||
|
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
|
||||||
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextInput source="contractor" label="Подрядная организация" />
|
||||||
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
||||||
|
<TextInput source="description" label="Описание работ / дефекта" multiline />
|
||||||
|
<TextInput source="notes" label="Примечания" multiline />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
46
client/src/resources/repair-order/RepairOrderEdit.tsx
Normal file
46
client/src/resources/repair-order/RepairOrderEdit.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
DateInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const repairKindChoices = [
|
||||||
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
|
{ id: 'TR', name: 'Текущий ремонт' },
|
||||||
|
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
|
||||||
|
{ id: 'KR', name: 'Капитальный ремонт' },
|
||||||
|
{ id: 'AR', name: 'Аварийный ремонт' },
|
||||||
|
{ id: 'MP', name: 'Метрологическая поверка' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Draft', name: 'Черновик' },
|
||||||
|
{ id: 'Approved', name: 'Утверждена' },
|
||||||
|
{ id: 'InWork', name: 'В работе' },
|
||||||
|
{ id: 'Done', name: 'Выполнена' },
|
||||||
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RepairOrderEdit = () => (
|
||||||
|
<Edit>
|
||||||
|
<SimpleForm>
|
||||||
|
<TextInput source="number" label="Номер заявки" isRequired />
|
||||||
|
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
|
||||||
|
<SelectInput optionText="name" isRequired />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
|
||||||
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextInput source="contractor" label="Подрядная организация" />
|
||||||
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
||||||
|
<TextInput source="description" label="Описание работ / дефекта" multiline />
|
||||||
|
<TextInput source="notes" label="Примечания" multiline />
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
40
client/src/resources/repair-order/RepairOrderList.tsx
Normal file
40
client/src/resources/repair-order/RepairOrderList.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
List,
|
||||||
|
Datagrid,
|
||||||
|
TextField,
|
||||||
|
DateField,
|
||||||
|
SelectField,
|
||||||
|
ReferenceField,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const repairKindChoices = [
|
||||||
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
|
{ id: 'TR', name: 'Текущий ремонт' },
|
||||||
|
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
|
||||||
|
{ id: 'KR', name: 'Капитальный ремонт' },
|
||||||
|
{ id: 'AR', name: 'Аварийный ремонт' },
|
||||||
|
{ id: 'MP', name: 'Метрологическая поверка' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Draft', name: 'Черновик' },
|
||||||
|
{ id: 'Approved', name: 'Утверждена' },
|
||||||
|
{ id: 'InWork', name: 'В работе' },
|
||||||
|
{ id: 'Done', name: 'Выполнена' },
|
||||||
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RepairOrderList = () => (
|
||||||
|
<List>
|
||||||
|
<Datagrid rowClick="show">
|
||||||
|
<TextField source="number" label="Номер" />
|
||||||
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
|
<TextField source="name" />
|
||||||
|
</ReferenceField>
|
||||||
|
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
||||||
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<DateField source="plannedAt" label="Плановая дата" />
|
||||||
|
<TextField source="contractor" label="Подрядчик" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
46
client/src/resources/repair-order/RepairOrderShow.tsx
Normal file
46
client/src/resources/repair-order/RepairOrderShow.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Show,
|
||||||
|
SimpleShowLayout,
|
||||||
|
TextField,
|
||||||
|
NumberField,
|
||||||
|
DateField,
|
||||||
|
SelectField,
|
||||||
|
ReferenceField,
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
const repairKindChoices = [
|
||||||
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
|
{ id: 'TR', name: 'Текущий ремонт' },
|
||||||
|
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
|
||||||
|
{ id: 'KR', name: 'Капитальный ремонт' },
|
||||||
|
{ id: 'AR', name: 'Аварийный ремонт' },
|
||||||
|
{ id: 'MP', name: 'Метрологическая поверка' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusChoices = [
|
||||||
|
{ id: 'Draft', name: 'Черновик' },
|
||||||
|
{ id: 'Approved', name: 'Утверждена' },
|
||||||
|
{ id: 'InWork', name: 'В работе' },
|
||||||
|
{ id: 'Done', name: 'Выполнена' },
|
||||||
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RepairOrderShow = () => (
|
||||||
|
<Show>
|
||||||
|
<SimpleShowLayout>
|
||||||
|
<TextField source="number" label="Номер заявки" />
|
||||||
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
|
<TextField source="name" />
|
||||||
|
</ReferenceField>
|
||||||
|
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
||||||
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
|
<DateField source="plannedAt" label="Плановая дата начала" />
|
||||||
|
<DateField source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateField source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextField source="contractor" label="Подрядная организация" />
|
||||||
|
<NumberField source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
||||||
|
<TextField source="description" label="Описание работ / дефекта" />
|
||||||
|
<TextField source="notes" label="Примечания" />
|
||||||
|
</SimpleShowLayout>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
1
client/src/vite-env.d.ts
vendored
Normal file
1
client/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
25
client/tsconfig.json
Normal file
25
client/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
11
client/tsconfig.node.json
Normal file
11
client/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
client/vite.config.ts
Normal file
7
client/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: toir-postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: toir
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
213
domain/dsl-spec.md
Normal file
213
domain/dsl-spec.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# DSL Language Specification
|
||||||
|
|
||||||
|
This document describes the DSL (Domain Specific Language) used to specify fullstack CRUD applications. The DSL has four layers: Domain, DTO, API, and UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DSL Grammar Concepts
|
||||||
|
|
||||||
|
## entity
|
||||||
|
|
||||||
|
An **entity** is a domain object that becomes a database table and a first-class resource in the backend and frontend.
|
||||||
|
|
||||||
|
```
|
||||||
|
entity Equipment {
|
||||||
|
attribute id { type uuid; key primary; }
|
||||||
|
attribute name { type string; is required; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Domain:** Defines the canonical model; one entity = one Prisma model, one NestJS module, one React Admin resource.
|
||||||
|
- **Naming:** PascalCase (e.g. `Equipment`, `EquipmentType`, `RepairOrder`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## attribute
|
||||||
|
|
||||||
|
An **attribute** is a field of an entity. It has a type and optional modifiers.
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute name {
|
||||||
|
description "Наименование";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
is unique;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modifiers:**
|
||||||
|
|
||||||
|
- `type` — required; one of: `string`, `uuid`, `integer`, `decimal`, `date`, `text`, or an enum name.
|
||||||
|
- `key primary` — this attribute is the primary key.
|
||||||
|
- `key foreign { relates Entity.field }` — foreign key to another entity's field.
|
||||||
|
- `is required` — non-nullable.
|
||||||
|
- `is unique` — unique constraint.
|
||||||
|
- `default Value` — default value (for enums or literals).
|
||||||
|
- `description "..."` — human-readable description.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## enum
|
||||||
|
|
||||||
|
An **enum** defines a fixed set of values. Used for attributes that can only take one of these values.
|
||||||
|
|
||||||
|
```
|
||||||
|
enum EquipmentStatus {
|
||||||
|
value Active { label "В эксплуатации"; }
|
||||||
|
value Repair { label "В ремонте"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **value** — identifier used in data and code.
|
||||||
|
- **label** — optional display label for UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## primary key
|
||||||
|
|
||||||
|
Exactly one attribute per entity must be marked as primary key.
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
key primary;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for a natural key:
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
key primary;
|
||||||
|
is required;
|
||||||
|
is unique;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## foreign key
|
||||||
|
|
||||||
|
A **foreign key** links to another entity's primary key. The attribute type must match the referenced primary key type.
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
key foreign {
|
||||||
|
relates EquipmentType.code;
|
||||||
|
}
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `relates Entity.attribute` — references `Entity`'s `attribute` (must be primary key).
|
||||||
|
- FK type must equal referenced PK type (e.g. `string` → `EquipmentType.code`, `uuid` → `Equipment.id`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## required
|
||||||
|
|
||||||
|
- **is required** — attribute is non-nullable in domain and (unless overridden) in DTOs.
|
||||||
|
- Absence of `is required` means the attribute is optional (nullable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## default
|
||||||
|
|
||||||
|
- **default Value** — applied when no value is provided (e.g. enum defaults like `default Active`).
|
||||||
|
- Value must exist in the enum when the attribute type is an enum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## map
|
||||||
|
|
||||||
|
Used in **DTO** and **UI** layers to bind a DTO/UI field to a domain entity attribute.
|
||||||
|
|
||||||
|
**In DTO:**
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ensures DTO attribute corresponds to an existing `Entity.attribute` and that types align.
|
||||||
|
|
||||||
|
**In UI:**
|
||||||
|
|
||||||
|
```
|
||||||
|
attribute Наименование {
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- UI label (e.g. "Наименование") maps to domain field `Equipment.name` for correct data binding and generation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DSL → System Component Mapping
|
||||||
|
|
||||||
|
## DSL → Prisma
|
||||||
|
|
||||||
|
| DSL Concept | Prisma Result |
|
||||||
|
| ------------ | --------------------------- |
|
||||||
|
| entity | model |
|
||||||
|
| attribute | field |
|
||||||
|
| enum | enum |
|
||||||
|
| key primary | @id or @id @default(uuid()) |
|
||||||
|
| key foreign | relation + references |
|
||||||
|
| type string | String |
|
||||||
|
| type uuid | String @id @default(uuid()) |
|
||||||
|
| type integer | Int |
|
||||||
|
| type decimal | Decimal |
|
||||||
|
| type date | DateTime |
|
||||||
|
| type text | String |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DSL → NestJS
|
||||||
|
|
||||||
|
| DSL Concept | NestJS Result |
|
||||||
|
| -------------- | ------------------------------------- |
|
||||||
|
| entity | One module (e.g. equipment.module.ts) |
|
||||||
|
| entity | Controller with CRUD endpoints |
|
||||||
|
| entity | Service with Prisma CRUD |
|
||||||
|
| DTO (Create) | create-{entity}.dto.ts |
|
||||||
|
| DTO (Update) | update-{entity}.dto.ts |
|
||||||
|
| DTO (Response) | Used for GET response shape |
|
||||||
|
|
||||||
|
API paths are derived from entity name: PascalCase → kebab-case, pluralized (e.g. `Equipment` → `/equipment`, `RepairOrder` → `/repair-orders`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DSL → React Admin
|
||||||
|
|
||||||
|
| DSL Concept | React Admin Result |
|
||||||
|
| --------------------- | ----------------------------------- |
|
||||||
|
| entity | Resource (name = kebab-case plural) |
|
||||||
|
| attribute | Form field / column |
|
||||||
|
| type string | TextInput, TextField |
|
||||||
|
| type integer/decimal | NumberInput, NumberField |
|
||||||
|
| type date | DateInput, DateField |
|
||||||
|
| enum | SelectInput with choices |
|
||||||
|
| foreign key | ReferenceInput, ReferenceField |
|
||||||
|
| UI attribute with map | Field with correct source |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DTO Mapping
|
||||||
|
|
||||||
|
- **map Entity.attribute** — DTO attribute corresponds to domain attribute; types must match.
|
||||||
|
- **Create DTO** — must not include generated primary keys (e.g. no `id` for uuid PK).
|
||||||
|
- **Update DTO** — all fields optional (nullable) for partial updates.
|
||||||
|
- **List response DTO** — must expose `data` (array) and `total` (integer) for React Admin compatibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI Mapping
|
||||||
|
|
||||||
|
- Each UI attribute should have **map Entity.attribute** so it binds to a real domain field.
|
||||||
|
- UI attribute name is the label (e.g. "Наименование"); **source** in generated components is the domain attribute name (e.g. `name`).
|
||||||
|
- Enums → SelectInput; foreign keys → ReferenceInput/ReferenceField.
|
||||||
57
examples/TOiR-ui.dsl
Normal file
57
examples/TOiR-ui.dsl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import ./TOiR;
|
||||||
|
|
||||||
|
ui UI.Equipment {
|
||||||
|
offset 600;
|
||||||
|
|
||||||
|
description "Единица оборудования — объект ремонта и технического обслуживания";
|
||||||
|
|
||||||
|
attribute Код {
|
||||||
|
map Equipment.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute ИнвентарныйНомер {
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute СерийныйНомер {
|
||||||
|
map Equipment.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute Наименование {
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связь с видом оборудования (справочник НСИ)
|
||||||
|
attribute Тип {
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute Статус {
|
||||||
|
map Equipment.status;
|
||||||
|
description "Текущий статус";
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute МестоТекущее {
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute ДатаВвода {
|
||||||
|
map Equipment.commissionedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute НаработкаВсего {
|
||||||
|
map Equipment.totalEngineHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute НаработкаТекущая {
|
||||||
|
map Equipment.engineHoursSinceLastRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute Ремонт {
|
||||||
|
map Equipment.lastRepairAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute Примечания {
|
||||||
|
map Equipment.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
811
examples/TOiR.api.dsl
Normal file
811
examples/TOiR.api.dsl
Normal file
@@ -0,0 +1,811 @@
|
|||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// DTO: Вид оборудования (EquipmentType)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.EquipmentType {
|
||||||
|
description "Вид оборудования — полный объект ответа";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
map EquipmentType.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование вида";
|
||||||
|
map EquipmentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
type string;
|
||||||
|
description "Производитель";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.manufacturer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.maintenanceIntervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.overhaulIntervalHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeCreate {
|
||||||
|
description "Вид оборудования — тело запроса на создание";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
is required;
|
||||||
|
map EquipmentType.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование вида";
|
||||||
|
is required;
|
||||||
|
map EquipmentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
type string;
|
||||||
|
description "Производитель";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.manufacturer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.maintenanceIntervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.overhaulIntervalHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeUpdate {
|
||||||
|
description "Вид оборудования — тело запроса на обновление (частичное)";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование вида";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
type string;
|
||||||
|
description "Производитель";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.manufacturer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.maintenanceIntervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
type integer;
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.overhaulIntervalHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeListResponse {
|
||||||
|
description "Список видов оборудования (формат React Admin)";
|
||||||
|
|
||||||
|
attribute data {
|
||||||
|
type DTO.EquipmentType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute total {
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// DTO: Оборудование (Equipment)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.Equipment {
|
||||||
|
description "Единица оборудования — полный объект ответа";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map Equipment.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
type string;
|
||||||
|
description "Инвентарный номер";
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
type string;
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
description "Текущий статус";
|
||||||
|
map Equipment.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
type string;
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
type date;
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.commissionedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
type decimal;
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.totalEngineHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.engineHoursSinceLastRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
type date;
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.lastRepairAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentCreate {
|
||||||
|
description "Единица оборудования — тело запроса на создание";
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
type string;
|
||||||
|
description "Инвентарный номер";
|
||||||
|
is required;
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
type string;
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
is required;
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
is required;
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
description "Текущий статус";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
type string;
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
type date;
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.commissionedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
type decimal;
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.totalEngineHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.engineHoursSinceLastRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
type date;
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.lastRepairAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentUpdate {
|
||||||
|
description "Единица оборудования — тело запроса на обновление (частичное)";
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
type string;
|
||||||
|
description "Инвентарный номер";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
type string;
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
description "Текущий статус";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
type string;
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
type date;
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.commissionedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
type decimal;
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.totalEngineHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.engineHoursSinceLastRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
type date;
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.lastRepairAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map Equipment.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentListResponse {
|
||||||
|
description "Список оборудования (формат React Admin)";
|
||||||
|
|
||||||
|
attribute data {
|
||||||
|
type DTO.Equipment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute total {
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// DTO: Заявка на ремонт (RepairOrder)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.RepairOrder {
|
||||||
|
description "Заявка на ремонт — полный объект ответа";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map RepairOrder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
type string;
|
||||||
|
description "Номер заявки";
|
||||||
|
map RepairOrder.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
description "Идентификатор единицы оборудования";
|
||||||
|
map RepairOrder.equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
type RepairKind;
|
||||||
|
description "Вид ремонта";
|
||||||
|
map RepairOrder.repairKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
description "Статус заявки";
|
||||||
|
map RepairOrder.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
type date;
|
||||||
|
description "Плановая дата начала";
|
||||||
|
map RepairOrder.plannedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
type string;
|
||||||
|
description "Подрядная организация (если внешний ремонт)";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.engineHoursAtRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
type text;
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderCreate {
|
||||||
|
description "Заявка на ремонт — тело запроса на создание";
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
type string;
|
||||||
|
description "Номер заявки";
|
||||||
|
is required;
|
||||||
|
map RepairOrder.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
description "Идентификатор единицы оборудования";
|
||||||
|
is required;
|
||||||
|
map RepairOrder.equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
type RepairKind;
|
||||||
|
description "Вид ремонта";
|
||||||
|
is required;
|
||||||
|
map RepairOrder.repairKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
description "Статус заявки";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
type date;
|
||||||
|
description "Плановая дата начала";
|
||||||
|
is required;
|
||||||
|
map RepairOrder.plannedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
type string;
|
||||||
|
description "Подрядная организация (если внешний ремонт)";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.engineHoursAtRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
type text;
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderUpdate {
|
||||||
|
description "Заявка на ремонт — тело запроса на обновление (частичное)";
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
type string;
|
||||||
|
description "Номер заявки";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
description "Идентификатор единицы оборудования";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
type RepairKind;
|
||||||
|
description "Вид ремонта";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.repairKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
description "Статус заявки";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
type date;
|
||||||
|
description "Плановая дата начала";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.plannedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
type date;
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
type string;
|
||||||
|
description "Подрядная организация (если внешний ремонт)";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
type decimal;
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.engineHoursAtRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
type text;
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
description "Примечания";
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderListResponse {
|
||||||
|
description "Список заявок на ремонт (формат React Admin)";
|
||||||
|
|
||||||
|
attribute data {
|
||||||
|
type DTO.RepairOrder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute total {
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API: Виды оборудования
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
api API.EquipmentTypes {
|
||||||
|
description "API управления справочником видов оборудования";
|
||||||
|
|
||||||
|
endpoint listEquipmentTypes {
|
||||||
|
label "GET /equipment-types";
|
||||||
|
description "Список видов оборудования (фильтры и пагинация — query-параметры)";
|
||||||
|
attribute response {
|
||||||
|
type DTO.EquipmentTypeListResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint getEquipmentType {
|
||||||
|
label "GET /equipment-types/{code}";
|
||||||
|
description "Получить вид оборудования по коду";
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
attribute response {
|
||||||
|
type DTO.EquipmentType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint createEquipmentType {
|
||||||
|
label "POST /equipment-types";
|
||||||
|
description "Создать вид оборудования";
|
||||||
|
attribute request {
|
||||||
|
type DTO.EquipmentTypeCreate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint updateEquipmentType {
|
||||||
|
label "PATCH /equipment-types/{code}";
|
||||||
|
description "Обновить вид оборудования";
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
attribute request {
|
||||||
|
type DTO.EquipmentTypeUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint deleteEquipmentType {
|
||||||
|
label "DELETE /equipment-types/{code}";
|
||||||
|
description "Удалить вид оборудования";
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API: Оборудование
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
api API.Equipment {
|
||||||
|
description "API управления оборудованием";
|
||||||
|
|
||||||
|
endpoint listEquipment {
|
||||||
|
label "GET /equipment";
|
||||||
|
description "Список оборудования (фильтры и пагинация — query-параметры)";
|
||||||
|
attribute response {
|
||||||
|
type DTO.EquipmentListResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint getEquipment {
|
||||||
|
label "GET /equipment/{id}";
|
||||||
|
description "Получить единицу оборудования по идентификатору";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
attribute response {
|
||||||
|
type DTO.Equipment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint createEquipment {
|
||||||
|
label "POST /equipment";
|
||||||
|
description "Создать единицу оборудования";
|
||||||
|
attribute request {
|
||||||
|
type DTO.EquipmentCreate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint updateEquipment {
|
||||||
|
label "PATCH /equipment/{id}";
|
||||||
|
description "Обновить единицу оборудования";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
attribute request {
|
||||||
|
type DTO.EquipmentUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint deleteEquipment {
|
||||||
|
label "DELETE /equipment/{id}";
|
||||||
|
description "Удалить единицу оборудования";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API: Заявки на ремонт
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
api API.RepairOrders {
|
||||||
|
description "API управления заявками на ремонт";
|
||||||
|
|
||||||
|
endpoint listRepairOrders {
|
||||||
|
label "GET /repair-orders";
|
||||||
|
description "Список заявок на ремонт (фильтры и пагинация — query-параметры)";
|
||||||
|
attribute response {
|
||||||
|
type DTO.RepairOrderListResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint getRepairOrder {
|
||||||
|
label "GET /repair-orders/{id}";
|
||||||
|
description "Получить заявку на ремонт по идентификатору";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
attribute response {
|
||||||
|
type DTO.RepairOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint createRepairOrder {
|
||||||
|
label "POST /repair-orders";
|
||||||
|
description "Создать заявку на ремонт";
|
||||||
|
attribute request {
|
||||||
|
type DTO.RepairOrderCreate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint updateRepairOrder {
|
||||||
|
label "PATCH /repair-orders/{id}";
|
||||||
|
description "Обновить заявку на ремонт";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
attribute request {
|
||||||
|
type DTO.RepairOrderUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint deleteRepairOrder {
|
||||||
|
label "DELETE /repair-orders/{id}";
|
||||||
|
description "Удалить заявку на ремонт";
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
examples/TOiR.domain.dsl
Normal file
257
examples/TOiR.domain.dsl
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/*
|
||||||
|
КИС ТОиР — демонстрационная схема доменной модели
|
||||||
|
Сущности: Equipment (Оборудование), EquipmentType (Вид оборудования), RepairOrder (Заявка на ремонт)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Перечисления
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
enum EquipmentStatus {
|
||||||
|
value Active {
|
||||||
|
label "В эксплуатации";
|
||||||
|
}
|
||||||
|
value Repair {
|
||||||
|
label "В ремонте";
|
||||||
|
}
|
||||||
|
value Reserve {
|
||||||
|
label "В резерве";
|
||||||
|
}
|
||||||
|
value WriteOff {
|
||||||
|
label "Списано";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepairKind {
|
||||||
|
value TO {
|
||||||
|
label "Техническое обслуживание";
|
||||||
|
}
|
||||||
|
value TR {
|
||||||
|
label "Текущий ремонт";
|
||||||
|
}
|
||||||
|
value TRE {
|
||||||
|
label "Текущий расширенный ремонт";
|
||||||
|
}
|
||||||
|
value KR {
|
||||||
|
label "Капитальный ремонт";
|
||||||
|
}
|
||||||
|
value AR {
|
||||||
|
label "Аварийный ремонт";
|
||||||
|
}
|
||||||
|
value MP {
|
||||||
|
label "Метрологическая поверка";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepairOrderStatus {
|
||||||
|
value Draft {
|
||||||
|
label "Черновик";
|
||||||
|
}
|
||||||
|
value Approved {
|
||||||
|
label "Утверждена";
|
||||||
|
}
|
||||||
|
value InWork {
|
||||||
|
label "В работе";
|
||||||
|
}
|
||||||
|
value Done {
|
||||||
|
label "Выполнена";
|
||||||
|
}
|
||||||
|
value Cancelled {
|
||||||
|
label "Отменена";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Справочник: Вид оборудования
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
entity EquipmentType {
|
||||||
|
description "Вид (марка) оборудования — нормативный справочник НСИ";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
key primary;
|
||||||
|
description "Код вида оборудования";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
is unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование вида";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
description "Производитель";
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нормативный межремонтный ресурс (моточасы)
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Основная сущность: Оборудование
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
entity Equipment {
|
||||||
|
description "Единица оборудования — объект ремонта и технического обслуживания";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
key primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
description "Инвентарный номер";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
is unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связь с видом оборудования (справочник НСИ)
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
key foreign {
|
||||||
|
relates EquipmentType.code;
|
||||||
|
}
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Текущий статус";
|
||||||
|
type EquipmentStatus;
|
||||||
|
default Active;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
type date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наработка фиксируется вручную или из производственной программы
|
||||||
|
attribute totalEngineHours {
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
type decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
type date;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Заявка на ремонт
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
entity RepairOrder {
|
||||||
|
description "Заявка на ремонт — формируется по ППР или по факту обнаруженного дефекта";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
key primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
description "Номер заявки";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
is unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
key foreign {
|
||||||
|
relates Equipment.id;
|
||||||
|
}
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
description "Вид ремонта";
|
||||||
|
type RepairKind;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
default Draft;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
description "Плановая дата начала";
|
||||||
|
type date;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
type date;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
type date;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
description "Подрядная организация (если внешний ремонт)";
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
type text;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
}
|
||||||
|
}
|
||||||
753
examples/TOiR.dto.dsl
Normal file
753
examples/TOiR.dto.dsl
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
/*
|
||||||
|
КИС ТОиР — DTO
|
||||||
|
Структуры данных для обмена через API
|
||||||
|
*/
|
||||||
|
|
||||||
|
//import ./TOiR;
|
||||||
|
//only external;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Общие
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.PageRequest {
|
||||||
|
description "Параметры постраничной выдачи";
|
||||||
|
|
||||||
|
attribute page {
|
||||||
|
description "Номер страницы (начиная с 0)";
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute size {
|
||||||
|
description "Размер страницы";
|
||||||
|
type integer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.Filter {
|
||||||
|
description "Элемент фильтра для запросов списка";
|
||||||
|
attribute field { type string; }
|
||||||
|
attribute operator { type string; }
|
||||||
|
attribute value { type string; }
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.PageInfo {
|
||||||
|
description "Метаданные постраничной выдачи";
|
||||||
|
attribute size { type integer; }
|
||||||
|
attribute number { type integer; }
|
||||||
|
attribute totalElements { type integer; }
|
||||||
|
attribute totalPages { type integer; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Вид оборудования (EquipmentType)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.EquipmentType {
|
||||||
|
description "Вид оборудования — response";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
type string;
|
||||||
|
map EquipmentType.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
map EquipmentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.manufacturer;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.maintenanceIntervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
map EquipmentType.overhaulIntervalHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeFilter {
|
||||||
|
description "Фильтры для списка видов оборудования";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
description "Частичное совпадение по коду";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Частичное совпадение по наименованию";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
description "Частичное совпадение по производителю";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeCreate {
|
||||||
|
description "Тело запроса на создание вида оборудования";
|
||||||
|
|
||||||
|
attribute code {
|
||||||
|
description "Код вида оборудования";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование вида";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
description "Производитель";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentTypeUpdate {
|
||||||
|
description "Тело запроса на обновление вида оборудования";
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование вида";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute manufacturer {
|
||||||
|
description "Производитель";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute maintenanceIntervalHours {
|
||||||
|
description "Периодичность ТО, моточасов";
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute overhaulIntervalHours {
|
||||||
|
description "Периодичность КР, моточасов";
|
||||||
|
type integer;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Оборудование (Equipment)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.EquipmentListItem {
|
||||||
|
description "Строка списка оборудования";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map Equipment.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
type string;
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
map Equipment.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentDetail {
|
||||||
|
description "Полная информация об оборудовании";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map Equipment.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
type string;
|
||||||
|
map Equipment.inventoryNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
map Equipment.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
map Equipment.equipmentTypeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
map Equipment.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.commissionedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.totalEngineHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.engineHoursSinceLastRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.lastRepairAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
map Equipment.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentFilter {
|
||||||
|
description "Фильтры для списка оборудования";
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
description "Частичное совпадение по инвентарному номеру";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
description "Частичное совпадение по заводскому номеру";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Частичное совпадение по наименованию";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
description "Точное совпадение по коду вида оборудования";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Фильтр по статусу";
|
||||||
|
type EquipmentStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
description "Частичное совпадение по месту эксплуатации";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentCreate {
|
||||||
|
description "Тело запроса на создание оборудования";
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
description "Инвентарный номер";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
description "Код вида оборудования";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Текущий статус";
|
||||||
|
type EquipmentStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.EquipmentUpdate {
|
||||||
|
description "Тело запроса на обновление оборудования";
|
||||||
|
|
||||||
|
attribute inventoryNumber {
|
||||||
|
description "Инвентарный номер";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute serialNumber {
|
||||||
|
description "Заводской (серийный) номер";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
description "Наименование единицы оборудования";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
description "Код вида оборудования";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Текущий статус";
|
||||||
|
type EquipmentStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute location {
|
||||||
|
description "Место эксплуатации / скважина / куст";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute commissionedAt {
|
||||||
|
description "Дата ввода в эксплуатацию";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute totalEngineHours {
|
||||||
|
description "Общая наработка, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursSinceLastRepair {
|
||||||
|
description "Наработка с последнего ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute lastRepairAt {
|
||||||
|
description "Дата последнего ремонта";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Заявка на ремонт (RepairOrder)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
dto DTO.RepairOrderListItem {
|
||||||
|
description "Строка списка заявок на ремонт";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map RepairOrder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
type string;
|
||||||
|
map RepairOrder.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
map RepairOrder.equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
type RepairKind;
|
||||||
|
map RepairOrder.repairKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
map RepairOrder.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
type date;
|
||||||
|
map RepairOrder.plannedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.contractor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderDetail {
|
||||||
|
description "Полная информация о заявке на ремонт";
|
||||||
|
|
||||||
|
attribute id {
|
||||||
|
type uuid;
|
||||||
|
map RepairOrder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
type string;
|
||||||
|
map RepairOrder.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
type uuid;
|
||||||
|
map RepairOrder.equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
type RepairKind;
|
||||||
|
map RepairOrder.repairKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type RepairOrderStatus;
|
||||||
|
map RepairOrder.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
type date;
|
||||||
|
map RepairOrder.plannedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.engineHoursAtRepair;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
map RepairOrder.notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderFilter {
|
||||||
|
description "Фильтры для списка заявок на ремонт";
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
description "Частичное совпадение по номеру заявки";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
description "Точное совпадение по идентификатору оборудования";
|
||||||
|
type uuid;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
description "Фильтр по виду ремонта";
|
||||||
|
type RepairKind;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Фильтр по статусу заявки";
|
||||||
|
type RepairOrderStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAtFrom {
|
||||||
|
description "Плановая дата начала ОТ";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAtTo {
|
||||||
|
description "Плановая дата начала ДО";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
description "Частичное совпадение по подрядчику";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderCreate {
|
||||||
|
description "Тело запроса на создание заявки на ремонт";
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
description "Номер заявки";
|
||||||
|
type string;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
description "Идентификатор оборудования";
|
||||||
|
type uuid;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
description "Вид ремонта";
|
||||||
|
type RepairKind;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Статус заявки";
|
||||||
|
type RepairOrderStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
description "Плановая дата начала";
|
||||||
|
type date;
|
||||||
|
is required;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
description "Подрядная организация";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dto DTO.RepairOrderUpdate {
|
||||||
|
description "Тело запроса на обновление заявки на ремонт";
|
||||||
|
|
||||||
|
attribute number {
|
||||||
|
description "Номер заявки";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute equipmentId {
|
||||||
|
description "Идентификатор оборудования";
|
||||||
|
type uuid;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute repairKind {
|
||||||
|
description "Вид ремонта";
|
||||||
|
type RepairKind;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
description "Статус заявки";
|
||||||
|
type RepairOrderStatus;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute plannedAt {
|
||||||
|
description "Плановая дата начала";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute startedAt {
|
||||||
|
description "Фактическая дата начала";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute completedAt {
|
||||||
|
description "Фактическая дата завершения";
|
||||||
|
type date;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute contractor {
|
||||||
|
description "Подрядная организация";
|
||||||
|
type string;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute engineHoursAtRepair {
|
||||||
|
description "Наработка на момент ремонта, моточасов";
|
||||||
|
type decimal;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute description {
|
||||||
|
description "Описание работ / дефекта";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute notes {
|
||||||
|
description "Примечания";
|
||||||
|
type text;
|
||||||
|
is nullable;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
frontend/architecture.md
Normal file
123
frontend/architecture.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Frontend Architecture
|
||||||
|
|
||||||
|
Frontend stack:
|
||||||
|
|
||||||
|
- React
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- React Admin
|
||||||
|
- shadcn/ui
|
||||||
|
|
||||||
|
The frontend is generated from the DSL and API specification.
|
||||||
|
|
||||||
|
Each entity becomes a React Admin resource.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
client/
|
||||||
|
src/
|
||||||
|
|
||||||
|
App.tsx
|
||||||
|
|
||||||
|
resources/
|
||||||
|
|
||||||
|
{entity}/
|
||||||
|
{entity}List.tsx
|
||||||
|
{entity}Create.tsx
|
||||||
|
{entity}Edit.tsx
|
||||||
|
{entity}Show.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource Registration
|
||||||
|
|
||||||
|
Each resource must be registered in App.tsx.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
<Resource
|
||||||
|
name="equipment"
|
||||||
|
list={EquipmentList}
|
||||||
|
create={EquipmentCreate}
|
||||||
|
edit={EquipmentEdit}
|
||||||
|
show={EquipmentShow}
|
||||||
|
/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Data Provider
|
||||||
|
|
||||||
|
React Admin uses the standard REST provider.
|
||||||
|
|
||||||
|
API format must follow:
|
||||||
|
|
||||||
|
GET /resource
|
||||||
|
GET /resource/:id
|
||||||
|
POST /resource
|
||||||
|
PATCH /resource/:id
|
||||||
|
DELETE /resource/:id
|
||||||
|
|
||||||
|
List response format:
|
||||||
|
|
||||||
|
{
|
||||||
|
data: [],
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Foreign Keys
|
||||||
|
|
||||||
|
Foreign keys must use ReferenceInput and ReferenceField.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Naming Conventions
|
||||||
|
|
||||||
|
## React component naming
|
||||||
|
|
||||||
|
Components are named after the entity in PascalCase. One entity = one resource with four main views.
|
||||||
|
|
||||||
|
- **List:** `{Entity}List.tsx` (e.g. `EquipmentList.tsx`, `RepairOrderList.tsx`)
|
||||||
|
- **Create:** `{Entity}Create.tsx` (e.g. `EquipmentCreate.tsx`)
|
||||||
|
- **Edit:** `{Entity}Edit.tsx` (e.g. `EquipmentEdit.tsx`)
|
||||||
|
- **Show:** `{Entity}Show.tsx` (e.g. `EquipmentShow.tsx`)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Equipment → `EquipmentList.tsx`, `EquipmentCreate.tsx`, `EquipmentEdit.tsx`, `EquipmentShow.tsx`
|
||||||
|
- EquipmentType → `EquipmentTypeList.tsx`, `EquipmentTypeCreate.tsx`, `EquipmentTypeEdit.tsx`, `EquipmentTypeShow.tsx`
|
||||||
|
- RepairOrder → `RepairOrderList.tsx`, `RepairOrderCreate.tsx`, `RepairOrderEdit.tsx`, `RepairOrderShow.tsx`
|
||||||
|
|
||||||
|
## Resource folder naming
|
||||||
|
|
||||||
|
Folder under `resources/` uses kebab-case, matching the React Admin resource name:
|
||||||
|
|
||||||
|
- `resources/equipment/`
|
||||||
|
- `resources/equipment-type/`
|
||||||
|
- `resources/repair-order/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resource Naming Rules
|
||||||
|
|
||||||
|
React Admin resource name (used in `<Resource name="..." />` and in `reference` for ReferenceInput) must match the API resource path (no leading slash, same segment string).
|
||||||
|
|
||||||
|
1. **Entity → resource name:** PascalCase entity name is converted to kebab-case and pluralized.
|
||||||
|
2. **Consistency with API:** The resource name must be the same as the backend path segment so that the data provider calls the correct URL.
|
||||||
|
|
||||||
|
| Entity (DSL) | Resource name (React Admin) | API path |
|
||||||
|
|----------------|-----------------------------|------------|
|
||||||
|
| Equipment | equipment | /equipment |
|
||||||
|
| EquipmentType | equipment-types | /equipment-types |
|
||||||
|
| RepairOrder | repair-orders | /repair-orders |
|
||||||
|
|
||||||
|
Examples in App.tsx:
|
||||||
|
- `<Resource name="equipment" list={EquipmentList} create={EquipmentCreate} edit={EquipmentEdit} show={EquipmentShow} />`
|
||||||
|
- `<Resource name="equipment-types" list={EquipmentTypeList} ... />`
|
||||||
|
- `<Resource name="repair-orders" list={RepairOrderList} ... />`
|
||||||
98
frontend/react-admin-rules.md
Normal file
98
frontend/react-admin-rules.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# DSL → React Admin Mapping
|
||||||
|
|
||||||
|
Entity attributes determine UI fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Type Mapping
|
||||||
|
|
||||||
|
| DSL Type | React Admin Component |
|
||||||
|
|---------|-----------------------|
|
||||||
|
| string | TextInput / TextField |
|
||||||
|
| integer | NumberInput |
|
||||||
|
| decimal | NumberInput |
|
||||||
|
| date | DateInput |
|
||||||
|
| enum | SelectInput |
|
||||||
|
| foreign key | ReferenceInput |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
DSL
|
||||||
|
|
||||||
|
attribute name {
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
React Admin
|
||||||
|
|
||||||
|
<TextInput source="name" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Enum Example
|
||||||
|
|
||||||
|
DSL
|
||||||
|
|
||||||
|
attribute status {
|
||||||
|
type EquipmentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
React Admin
|
||||||
|
|
||||||
|
<SelectInput source="status" choices={statusChoices} />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Foreign Key Example
|
||||||
|
|
||||||
|
DSL
|
||||||
|
|
||||||
|
attribute equipmentTypeCode {
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
|
||||||
|
React Admin
|
||||||
|
|
||||||
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# React Admin ID Field Requirement
|
||||||
|
|
||||||
|
React Admin requires every record in list and detail responses to contain a field named **`id`**. It uses this field for resource identity, cache keys, and references.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. Every record returned by the API must contain an **`id`** field.
|
||||||
|
2. If the DSL primary key is not named `id`, the generator must **map** the primary key value to an `id` field in the API response (backend) or in a frontend adapter.
|
||||||
|
3. The `id` field must contain the **value of the primary key** (e.g. uuid string, or `code` value for EquipmentType).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
DSL entity with primary key `code`:
|
||||||
|
|
||||||
|
```
|
||||||
|
entity EquipmentType {
|
||||||
|
attribute code {
|
||||||
|
key primary;
|
||||||
|
type string;
|
||||||
|
}
|
||||||
|
attribute name { type string; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
API response must include `id` so React Admin can identify the record:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "pump",
|
||||||
|
"code": "pump",
|
||||||
|
"name": "Pump"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the response only had `{ "code": "pump", "name": "Pump" }`, React Admin would not work correctly because it expects `id`. The backend or frontend adapter must therefore set `id: record.code` (or equivalent) when the primary key is not `id`.
|
||||||
|
|
||||||
|
This rule ensures compatibility with React Admin resource identity handling.
|
||||||
365
general-prompt.md
Normal file
365
general-prompt.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
ROLE
|
||||||
|
|
||||||
|
You are a Staff-level Fullstack Platform Engineer.
|
||||||
|
|
||||||
|
Your task is to generate a fully runnable fullstack CRUD application from the DSL context of this repository.
|
||||||
|
|
||||||
|
Use context7.
|
||||||
|
|
||||||
|
Follow official best practices from:
|
||||||
|
|
||||||
|
NestJS documentation
|
||||||
|
|
||||||
|
Prisma documentation
|
||||||
|
|
||||||
|
React Admin documentation
|
||||||
|
|
||||||
|
Docker documentation
|
||||||
|
|
||||||
|
The generated application must run without manual fixes.
|
||||||
|
|
||||||
|
PROJECT CONTEXT
|
||||||
|
|
||||||
|
You must read the project documentation in the following strict order:
|
||||||
|
|
||||||
|
domain/dsl-spec.md
|
||||||
|
|
||||||
|
examples/\*.dsl
|
||||||
|
|
||||||
|
backend/architecture.md
|
||||||
|
|
||||||
|
backend/prisma-rules.md
|
||||||
|
|
||||||
|
backend/prisma-service.md
|
||||||
|
|
||||||
|
backend/service-rules.md
|
||||||
|
|
||||||
|
backend/runtime-rules.md
|
||||||
|
|
||||||
|
backend/database-runtime.md
|
||||||
|
|
||||||
|
backend/seed-rules.md
|
||||||
|
|
||||||
|
frontend/architecture.md
|
||||||
|
|
||||||
|
frontend/react-admin-rules.md
|
||||||
|
|
||||||
|
generation/scaffolding-rules.md
|
||||||
|
|
||||||
|
generation/backend-generation.md
|
||||||
|
|
||||||
|
generation/frontend-generation.md
|
||||||
|
|
||||||
|
generation/runtime-bootstrap.md
|
||||||
|
|
||||||
|
generation/post-generation-validation.md
|
||||||
|
|
||||||
|
Do not ignore any rules defined in these documents.
|
||||||
|
|
||||||
|
GOAL
|
||||||
|
|
||||||
|
Generate a DSL-driven fullstack CRUD system.
|
||||||
|
|
||||||
|
Stack:
|
||||||
|
|
||||||
|
Backend
|
||||||
|
|
||||||
|
Node.js
|
||||||
|
|
||||||
|
NestJS
|
||||||
|
|
||||||
|
Prisma ORM
|
||||||
|
|
||||||
|
PostgreSQL
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
|
||||||
|
React
|
||||||
|
|
||||||
|
Vite
|
||||||
|
|
||||||
|
React Admin
|
||||||
|
|
||||||
|
MUI
|
||||||
|
|
||||||
|
shadcn/ui
|
||||||
|
|
||||||
|
PROJECT STRUCTURE
|
||||||
|
|
||||||
|
Root
|
||||||
|
docker-compose.yml
|
||||||
|
server/
|
||||||
|
client/
|
||||||
|
|
||||||
|
Backend
|
||||||
|
server/
|
||||||
|
src/
|
||||||
|
modules/{entity}/
|
||||||
|
prisma/schema.prisma
|
||||||
|
prisma/seed.ts
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
client/src/resources/{entity}/
|
||||||
|
client/src/App.tsx
|
||||||
|
client/src/dataProvider.ts
|
||||||
|
|
||||||
|
STEP 1 — Parse DSL
|
||||||
|
|
||||||
|
Parse all DSL files and extract:
|
||||||
|
|
||||||
|
Entities
|
||||||
|
Attributes
|
||||||
|
Primary keys
|
||||||
|
Foreign keys
|
||||||
|
Enums
|
||||||
|
|
||||||
|
Respect the DSL specification.
|
||||||
|
|
||||||
|
STEP 2 — CLI scaffolding
|
||||||
|
|
||||||
|
Use official CLIs.
|
||||||
|
|
||||||
|
Backend
|
||||||
|
npx @nestjs/cli@10.3.2 new server --package-manager npm --skip-git
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
npm create vite@5.2.0 client -- --template react-ts
|
||||||
|
|
||||||
|
STEP 3 — Install dependencies
|
||||||
|
|
||||||
|
Backend
|
||||||
|
|
||||||
|
@prisma/client
|
||||||
|
prisma
|
||||||
|
@nestjs/config
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
|
||||||
|
react-admin
|
||||||
|
ra-data-simple-rest
|
||||||
|
@mui/material
|
||||||
|
@emotion/react
|
||||||
|
@emotion/styled
|
||||||
|
|
||||||
|
STEP 4 — Generate Prisma schema
|
||||||
|
|
||||||
|
From DSL domain generate:
|
||||||
|
|
||||||
|
models
|
||||||
|
|
||||||
|
enums
|
||||||
|
|
||||||
|
relations
|
||||||
|
|
||||||
|
primary keys
|
||||||
|
|
||||||
|
Type mapping
|
||||||
|
|
||||||
|
decimal → Decimal
|
||||||
|
date → DateTime
|
||||||
|
|
||||||
|
DTO mapping
|
||||||
|
|
||||||
|
decimal → string
|
||||||
|
date → ISO string
|
||||||
|
|
||||||
|
STEP 5 — Generate NestJS modules
|
||||||
|
|
||||||
|
Per entity generate:
|
||||||
|
|
||||||
|
module
|
||||||
|
controller
|
||||||
|
service
|
||||||
|
dto
|
||||||
|
|
||||||
|
Controller routes
|
||||||
|
|
||||||
|
GET /resource
|
||||||
|
GET /resource/:pk
|
||||||
|
POST /resource
|
||||||
|
PATCH /resource/:pk
|
||||||
|
DELETE /resource/:pk
|
||||||
|
|
||||||
|
Path parameter must match the DSL primary key name.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
/equipment/:id
|
||||||
|
/equipment-types/:code
|
||||||
|
/repair-orders/:id
|
||||||
|
|
||||||
|
STEP 6 — Generate Service Layer
|
||||||
|
|
||||||
|
Service layer must follow backend/service-rules.md.
|
||||||
|
|
||||||
|
Important rule:
|
||||||
|
|
||||||
|
React Admin sends the id field in update payloads even when the primary key is not named id.
|
||||||
|
|
||||||
|
Therefore update payload must be sanitized before passing data to Prisma.
|
||||||
|
|
||||||
|
Services MUST NOT pass raw request DTO directly into Prisma.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
|
||||||
|
prisma.entity.update({
|
||||||
|
where,
|
||||||
|
data: dto
|
||||||
|
})
|
||||||
|
|
||||||
|
Correct pattern:
|
||||||
|
|
||||||
|
const { id, <primaryKey>, ...data } = dto
|
||||||
|
|
||||||
|
return prisma.entity.update({
|
||||||
|
where,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
||||||
|
Example (PK = code)
|
||||||
|
|
||||||
|
const { id, code, ...data } = dto
|
||||||
|
|
||||||
|
return prisma.equipmentType.update({
|
||||||
|
where: { code },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
||||||
|
Example (PK = id)
|
||||||
|
|
||||||
|
const { id: \_pk, ...data } = dto
|
||||||
|
|
||||||
|
return prisma.entity.update({
|
||||||
|
where: { id },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
||||||
|
Rules
|
||||||
|
|
||||||
|
Update payload passed to Prisma must not contain:
|
||||||
|
|
||||||
|
id
|
||||||
|
primary key attribute
|
||||||
|
readonly attributes
|
||||||
|
|
||||||
|
STEP 7 — Generate PrismaService
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
|
||||||
|
extends PrismaClient
|
||||||
|
implements OnModuleInit
|
||||||
|
await this.$connect()
|
||||||
|
|
||||||
|
Do NOT use
|
||||||
|
|
||||||
|
beforeExit
|
||||||
|
|
||||||
|
STEP 8 — Generate runtime infrastructure
|
||||||
|
|
||||||
|
Create
|
||||||
|
|
||||||
|
server/.env
|
||||||
|
server/.env.example
|
||||||
|
|
||||||
|
DATABASE_URL example
|
||||||
|
|
||||||
|
postgresql://postgres:postgres@localhost:5432/toir
|
||||||
|
|
||||||
|
Add to package.json
|
||||||
|
|
||||||
|
postinstall: prisma generate
|
||||||
|
|
||||||
|
STEP 9 — Database runtime
|
||||||
|
|
||||||
|
Generate root
|
||||||
|
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
PostgreSQL container
|
||||||
|
|
||||||
|
postgres:16
|
||||||
|
port 5432
|
||||||
|
|
||||||
|
STEP 10 — Generate seed
|
||||||
|
|
||||||
|
Create
|
||||||
|
|
||||||
|
server/prisma/seed.ts
|
||||||
|
|
||||||
|
Seed minimal data for
|
||||||
|
|
||||||
|
EquipmentType
|
||||||
|
Equipment
|
||||||
|
RepairOrder
|
||||||
|
|
||||||
|
Add to package.json
|
||||||
|
|
||||||
|
prisma.seed
|
||||||
|
|
||||||
|
STEP 11 — Generate React Admin
|
||||||
|
|
||||||
|
For each entity generate
|
||||||
|
|
||||||
|
Field mapping
|
||||||
|
|
||||||
|
string → TextInput
|
||||||
|
number → NumberInput
|
||||||
|
date → DateInput
|
||||||
|
enum → SelectInput
|
||||||
|
FK → ReferenceInput
|
||||||
|
|
||||||
|
API responses MUST contain
|
||||||
|
|
||||||
|
If PK ≠ id, map primary key to id.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
{
|
||||||
|
id: record.code,
|
||||||
|
code: record.code
|
||||||
|
}
|
||||||
|
|
||||||
|
STEP 12 — Validation
|
||||||
|
|
||||||
|
Verify
|
||||||
|
|
||||||
|
docker-compose.yml exists
|
||||||
|
database container starts
|
||||||
|
prisma migrate dev works
|
||||||
|
prisma db seed works
|
||||||
|
API responds /health
|
||||||
|
React Admin receives id
|
||||||
|
update services sanitize payload before Prisma
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
Provide
|
||||||
|
|
||||||
|
FULLSTACK GENERATION REPORT
|
||||||
|
|
||||||
|
Include
|
||||||
|
|
||||||
|
1 Parsed DSL
|
||||||
|
2 Prisma models
|
||||||
|
3 Backend modules
|
||||||
|
4 API endpoints
|
||||||
|
5 React Admin resources
|
||||||
|
6 Runtime configuration
|
||||||
|
7 Validation results
|
||||||
|
|
||||||
|
RUN INSTRUCTIONS
|
||||||
|
|
||||||
|
The generated application must run successfully with
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
npx prisma migrate dev
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
138
generation/backend-generation.md
Normal file
138
generation/backend-generation.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Backend Generation Process
|
||||||
|
|
||||||
|
Backend generation follows a pipeline aligned with runtime and validation docs:
|
||||||
|
|
||||||
|
DSL
|
||||||
|
↓
|
||||||
|
CLI scaffolding
|
||||||
|
↓
|
||||||
|
code generation
|
||||||
|
↓
|
||||||
|
runtime infrastructure
|
||||||
|
↓
|
||||||
|
database runtime
|
||||||
|
↓
|
||||||
|
migration
|
||||||
|
↓
|
||||||
|
seed
|
||||||
|
↓
|
||||||
|
validation
|
||||||
|
|
||||||
|
Follow **backend/runtime-rules.md**, **backend/prisma-rules.md**, **backend/prisma-service.md**, **backend/database-runtime.md**, **backend/seed-rules.md**, and **backend/service-rules.md**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 1 — Parse DSL
|
||||||
|
|
||||||
|
Read DSL inputs and extract:
|
||||||
|
|
||||||
|
- entities
|
||||||
|
- attributes (including primary key attribute name per entity)
|
||||||
|
- enums
|
||||||
|
- foreign keys
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 2 — CLI scaffolding
|
||||||
|
|
||||||
|
Use official CLIs before generating backend code:
|
||||||
|
|
||||||
|
- NestJS project scaffold in `server/` (see `generation/scaffolding-rules.md`)
|
||||||
|
- Install backend dependencies (`@prisma/client`, `prisma`, `@nestjs/config`, and seed runner when needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 3 — Code generation
|
||||||
|
|
||||||
|
Generate backend source artifacts:
|
||||||
|
|
||||||
|
1. **Prisma schema** (`server/prisma/schema.prisma`) from domain DSL:
|
||||||
|
- attributes
|
||||||
|
- primary keys
|
||||||
|
- relations
|
||||||
|
- enums
|
||||||
|
2. **NestJS modules** per entity:
|
||||||
|
- module
|
||||||
|
- controller (path params use actual PK name: `:id`, `:code`, etc.)
|
||||||
|
- service (must sanitize update payload before Prisma — see **backend/service-rules.md**)
|
||||||
|
3. **DTO files**:
|
||||||
|
- `create-entity.dto.ts`
|
||||||
|
- `update-entity.dto.ts`
|
||||||
|
- `entity.response.dto.ts` (or equivalent)
|
||||||
|
4. **PrismaService**:
|
||||||
|
- `OnModuleInit` + `await this.$connect()`
|
||||||
|
- no `beforeExit` hook
|
||||||
|
5. **Service update methods**: Sanitize update payload before passing to Prisma (remove `id`, primary key, and readonly attributes from `data`). Do not pass the raw request body as `data` to `prisma.*.update()`.
|
||||||
|
|
||||||
|
Use mapping rules from `backend/prisma-rules.md`:
|
||||||
|
- DSL `decimal` -> DTO `string`
|
||||||
|
- DSL `date` -> DTO `string` (ISO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 4 — Runtime infrastructure
|
||||||
|
|
||||||
|
Generate runtime config files:
|
||||||
|
|
||||||
|
- `server/.env`
|
||||||
|
- `server/.env.example`
|
||||||
|
- `server/package.json` lifecycle:
|
||||||
|
- `"postinstall": "prisma generate"`
|
||||||
|
- `server/package.json` Prisma seed:
|
||||||
|
- `"prisma": { "seed": "ts-node prisma/seed.ts" }` (or `tsx` variant by project standard)
|
||||||
|
|
||||||
|
Commands that must be supported/documented:
|
||||||
|
|
||||||
|
- `npx prisma generate`
|
||||||
|
- `npx prisma migrate dev`
|
||||||
|
- `npx prisma db seed`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 5 — Database runtime
|
||||||
|
|
||||||
|
Generator must create `docker-compose.yml` at the **project root** with PostgreSQL.
|
||||||
|
|
||||||
|
Minimum required compose characteristics:
|
||||||
|
|
||||||
|
- `services.postgres`
|
||||||
|
- `image: postgres:16`
|
||||||
|
- `ports: ["5432:5432"]`
|
||||||
|
|
||||||
|
Credentials/database in compose must match `DATABASE_URL`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 6 — Migration
|
||||||
|
|
||||||
|
Apply schema to development database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 7 — Seed
|
||||||
|
|
||||||
|
Run development seed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
Seed file location: `server/prisma/seed.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 8 — Validation
|
||||||
|
|
||||||
|
Run runtime and contract checks from `generation/post-generation-validation.md`, including:
|
||||||
|
|
||||||
|
- docker-compose exists and DB container starts
|
||||||
|
- Prisma lifecycle commands succeed
|
||||||
|
- seed runs
|
||||||
|
- `/health` responds
|
||||||
|
- React Admin receives `id` in every record
|
||||||
89
generation/dev-workflow.md
Normal file
89
generation/dev-workflow.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Developer Workflow
|
||||||
|
|
||||||
|
This document describes the **developer workflow** for running a generated fullstack application locally. The generator must produce a project that supports this workflow so the app is **fully runnable** after generation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** (LTS, e.g. 18+)
|
||||||
|
- **npm**
|
||||||
|
- **Docker** and **Docker Compose** (for the development database)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workflow Steps
|
||||||
|
|
||||||
|
## 1. Start the database
|
||||||
|
|
||||||
|
From the **project root**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the PostgreSQL container defined in `docker-compose.yml`. Wait a few seconds for the database to accept connections.
|
||||||
|
|
||||||
|
Verify (optional):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Backend setup and start
|
||||||
|
|
||||||
|
From the **server** directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npm install` — installs dependencies and runs `postinstall` (for example `prisma generate`).
|
||||||
|
- `npx prisma generate` — explicitly generates Prisma client.
|
||||||
|
- `npx prisma migrate dev` — creates/applies migrations.
|
||||||
|
- `npx prisma db seed` — inserts minimal development data.
|
||||||
|
- `npm run start` — starts NestJS backend.
|
||||||
|
|
||||||
|
The API should be available at the configured port (e.g. `http://localhost:3000`). Verify with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{ "status": "ok" }` (or equivalent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Frontend setup and start
|
||||||
|
|
||||||
|
In a **separate terminal**, from the **project root**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npm install` — installs frontend dependencies.
|
||||||
|
- `npm run dev` — starts the Vite dev server (e.g. `http://localhost:5173`).
|
||||||
|
|
||||||
|
Open the Vite URL in a browser; the React Admin app should load and use the backend API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
| Step | Command / location |
|
||||||
|
|------|---------------------|
|
||||||
|
| Start database | From root: `docker compose up -d` |
|
||||||
|
| Backend setup/start | `cd server && npm install && npx prisma generate && npx prisma migrate dev && npx prisma db seed && npm run start` |
|
||||||
|
| Frontend setup/start | `cd client && npm install && npm run dev` |
|
||||||
|
|
||||||
|
The generator must produce all required artifacts (docker-compose, env, schema, migrations, seed, health endpoint) so that this workflow succeeds and the development environment is fully runnable.
|
||||||
36
generation/frontend-generation.md
Normal file
36
generation/frontend-generation.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Frontend Generation Process
|
||||||
|
|
||||||
|
Frontend generation uses the DSL and API specification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 1 — Parse DSL
|
||||||
|
|
||||||
|
Extract entities and attributes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 2 — Generate React Admin Resources
|
||||||
|
|
||||||
|
For each entity create:
|
||||||
|
|
||||||
|
EntityList.tsx
|
||||||
|
EntityCreate.tsx
|
||||||
|
EntityEdit.tsx
|
||||||
|
EntityShow.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 3 — Map Fields
|
||||||
|
|
||||||
|
Map DSL attributes to React Admin components.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Step 4 — Register Resources
|
||||||
|
|
||||||
|
Register resources in App.tsx.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
<Resource name="equipment" ... />
|
||||||
160
generation/post-generation-validation.md
Normal file
160
generation/post-generation-validation.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Post-Generation Validation
|
||||||
|
|
||||||
|
After generating the backend or fullstack application, run these checks to ensure the project will run without runtime errors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validation Checklist
|
||||||
|
|
||||||
|
## 1. Environment file
|
||||||
|
|
||||||
|
- [ ] **`.env` exists** in the backend root (e.g. `server/.env`).
|
||||||
|
- [ ] **`.env.example` exists** with at least `DATABASE_URL` and a placeholder value.
|
||||||
|
- [ ] **`DATABASE_URL`** is present in `.env` (or documented in `.env.example` so the user can copy and set it).
|
||||||
|
|
||||||
|
**Failure symptom:** `Environment variable not found: DATABASE_URL` at startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. PrismaService implementation
|
||||||
|
|
||||||
|
- [ ] A **PrismaService** (or equivalent) class exists and extends `PrismaClient`.
|
||||||
|
- [ ] It implements **`OnModuleInit`** and calls **`await this.$connect()`** in `onModuleInit()`.
|
||||||
|
- [ ] It does **not** use **`this.$on('beforeExit', ...)`** or any `beforeExit` hook.
|
||||||
|
|
||||||
|
**Failure symptom:** Deprecation/runtime errors in Prisma 5 when using `beforeExit`.
|
||||||
|
|
||||||
|
**Reference:** `backend/prisma-service.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Prisma client lifecycle
|
||||||
|
|
||||||
|
- [ ] **`package.json`** includes a script that runs Prisma client generation:
|
||||||
|
- Either **`"postinstall": "prisma generate"`** (or `npx prisma generate`),
|
||||||
|
- Or clear documentation to run **`npx prisma generate`** after install.
|
||||||
|
- [ ] After schema generation or change, **`npx prisma generate`** has been run (or will run via postinstall).
|
||||||
|
|
||||||
|
**Failure symptom:** `Cannot find module '@prisma/client'` or missing types at build/run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Database migration
|
||||||
|
|
||||||
|
- [ ] **Migration workflow** is documented (e.g. in README or generation docs).
|
||||||
|
- [ ] Instruction to run **`npx prisma migrate dev`** (or `prisma migrate deploy` for production) after first generation or schema change.
|
||||||
|
- [ ] Generation pipeline (see `generation/backend-generation.md`) includes or documents the migration step.
|
||||||
|
|
||||||
|
**Failure symptom:** Tables do not exist; Prisma errors on first query.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. REST route parameters
|
||||||
|
|
||||||
|
- [ ] For each entity, path parameters use the **correct primary key name** from the DSL.
|
||||||
|
- [ ] **Entity with PK `id` (uuid):** routes use **`/:id`** (e.g. `GET /equipment/:id`, `PATCH /equipment/:id`).
|
||||||
|
- [ ] **Entity with non-`id` primary key (e.g. `code`):** routes use **`/:code`** (or the actual PK attribute name), e.g. `GET /equipment-types/:code`, **not** `GET /equipment-types/:id`.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
| Entity | Primary key | Correct path | Incorrect path |
|
||||||
|
| ------------- | ----------- | ---------------------- | -------------------- |
|
||||||
|
| Equipment | id | /equipment/:id | — |
|
||||||
|
| EquipmentType | code | /equipment-types/:code | /equipment-types/:id |
|
||||||
|
| RepairOrder | id | /repair-orders/:id | — |
|
||||||
|
|
||||||
|
**Failure symptom:** Controller expects `params.id` but route is defined with `:code`; or vice versa; 404 or wrong resource updated.
|
||||||
|
|
||||||
|
**Reference:** `backend/architecture.md` — API path rules for non-id primary keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. DTO type mapping (serialization)
|
||||||
|
|
||||||
|
- [ ] **DSL `decimal`** → In DTO/API response, use **`string`** (or a type that serializes to string), not Prisma `Decimal`, to avoid JSON serialization issues.
|
||||||
|
- [ ] **DSL `date`** → In DTO/API response, use **`string`** (ISO 8601) or ensure DateTime is serialized to string, so React Admin and JSON consumers receive a string.
|
||||||
|
|
||||||
|
**Reference:** `backend/prisma-rules.md` — DTO type mapping table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. React Admin ID field in API responses
|
||||||
|
|
||||||
|
- [ ] **Every API response object** (list items and single-resource GET) contains a field named **`id`**.
|
||||||
|
- [ ] If the entity primary key is **not** named `id`, the response must **map** the primary key to `id`: e.g. `id: record.code` for EquipmentType, so the payload includes both `id` and `code` (or at least `id` with the PK value).
|
||||||
|
- [ ] The `id` field value must be the **primary key value** (string or uuid as appropriate).
|
||||||
|
|
||||||
|
**Failure symptom:** React Admin fails to identify records, breaks cache/references, or throws when expecting `record.id`.
|
||||||
|
|
||||||
|
**Reference:** `frontend/react-admin-rules.md` — React Admin ID Field Requirement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Update payload sanitization (service layer)
|
||||||
|
|
||||||
|
- [ ] **Update endpoint must not pass `id` (or primary key) in Prisma `data`.** The service must sanitize the incoming DTO before calling `prisma.*.update({ where, data })`: remove `id`, remove the entity primary key field (e.g. `code`), and remove any readonly attributes. Only updatable fields should be passed as `data`.
|
||||||
|
- [ ] Generated update methods follow the pattern from **backend/service-rules.md** (e.g. `const { id, code, ...data } = dto` then pass `data` to Prisma).
|
||||||
|
|
||||||
|
**Failure symptom:** Prisma throws when `data` contains `id` or another field that is not on the model or not writable (e.g. entity with PK `code` receives body with `id` from React Admin).
|
||||||
|
|
||||||
|
**Reference:** `backend/service-rules.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Database runtime (docker-compose)
|
||||||
|
|
||||||
|
- [ ] **`docker-compose.yml` exists** at the project root (or documented location).
|
||||||
|
- [ ] It defines a **PostgreSQL** service with image (e.g. `postgres:16`), port `5432`, and credentials/DB name matching `DATABASE_URL` in `server/.env`.
|
||||||
|
- [ ] **Database container starts:** `docker compose up -d` runs without error and the container is reachable on the configured port.
|
||||||
|
|
||||||
|
**Failure symptom:** `PrismaClientInitializationError P1001: Can't reach database server at localhost:5432`.
|
||||||
|
|
||||||
|
**Reference:** `backend/database-runtime.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Migrations and seed
|
||||||
|
|
||||||
|
- [ ] **`npx prisma migrate dev`** runs successfully from `server/` when the database is up (schema is applied or created).
|
||||||
|
- [ ] **Seed script** exists at `server/prisma/seed.ts` (or equivalent) and creates minimal sample data (e.g. one EquipmentType, one Equipment, one RepairOrder).
|
||||||
|
- [ ] **`npx prisma db seed`** runs without error (package.json has `prisma.seed` configured and seed runner installed).
|
||||||
|
|
||||||
|
**Reference:** `backend/seed-rules.md`, `generation/runtime-bootstrap.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Health endpoint
|
||||||
|
|
||||||
|
- [ ] Backend exposes **GET /health** (or equivalent health route).
|
||||||
|
- [ ] **API responds to /health:** With backend running, `GET http://localhost:<port>/health` returns HTTP 200 and a body such as `{ "status": "ok" }`.
|
||||||
|
|
||||||
|
**Reference:** `backend/architecture.md` — Health Endpoint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary Table
|
||||||
|
|
||||||
|
| Check | Required artifact / rule |
|
||||||
|
| ------------------- | ---------------------------------------------- |
|
||||||
|
| .env | File exists with DATABASE_URL |
|
||||||
|
| DATABASE_URL | Present in .env or .env.example |
|
||||||
|
| PrismaService | OnModuleInit + $connect(); no beforeExit |
|
||||||
|
| prisma generate | postinstall script or documented step |
|
||||||
|
| Migration | Documented step: prisma migrate dev |
|
||||||
|
| REST path params | Use entity PK name (:id or :code, etc.) |
|
||||||
|
| Decimal/Date in DTO | Map to string for serialization |
|
||||||
|
| API response `id` | Every record has `id`; if PK ≠ id, map PK → id |
|
||||||
|
| Update payload | Service strips `id`, PK, readonly from data before Prisma |
|
||||||
|
| docker-compose.yml | Exists at project root; PostgreSQL service |
|
||||||
|
| Database container | Starts with `docker compose up -d` |
|
||||||
|
| prisma migrate dev | Runs successfully from server/ |
|
||||||
|
| Seed script | Exists; prisma db seed runs |
|
||||||
|
| GET /health | Backend responds with 200 and status payload |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Integration with generation pipeline
|
||||||
|
|
||||||
|
1. **Backend generation** (see `generation/backend-generation.md`) should produce artifacts that satisfy the above by default.
|
||||||
|
2. After generation, run this checklist manually or via a script that parses generated code and config.
|
||||||
|
3. If any check fails, the AI context or generator should be updated so that future runs pass.
|
||||||
64
generation/runtime-bootstrap.md
Normal file
64
generation/runtime-bootstrap.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Runtime Bootstrap
|
||||||
|
|
||||||
|
After project generation, the following commands must work in order so that the application runs without manual database provisioning or ad-hoc steps.
|
||||||
|
|
||||||
|
The generator must produce a **runnable development environment**: backend, frontend, and database must all be startable via a documented sequence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bootstrap Sequence
|
||||||
|
|
||||||
|
## 1. Start the database
|
||||||
|
|
||||||
|
From the **project root**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the PostgreSQL container. The backend will connect to it using `DATABASE_URL` from `server/.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Backend setup and start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npm install` — installs dependencies and runs `postinstall` (e.g. `prisma generate`) if configured.
|
||||||
|
- `npx prisma generate` — ensures Prisma client is generated explicitly after install/schema changes.
|
||||||
|
- `npx prisma migrate dev` — creates/applies migrations and ensures the database schema exists.
|
||||||
|
- `npx prisma db seed` — populates minimal development data for immediate UI/API usage.
|
||||||
|
- `npm run start` — starts the NestJS server (default port e.g. 3000).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Frontend setup and start
|
||||||
|
|
||||||
|
In a separate terminal, from the **project root**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npm run dev` — starts the Vite dev server (e.g. http://localhost:5173).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Success Criteria
|
||||||
|
|
||||||
|
After running the above:
|
||||||
|
|
||||||
|
- Database container is running; Prisma can connect.
|
||||||
|
- Backend responds (e.g. `GET /health` returns `{ "status": "ok" }`).
|
||||||
|
- Frontend loads and can call the backend API.
|
||||||
|
|
||||||
|
The generator is responsible for producing all artifacts (docker-compose, schema, migrations, seed, env, health endpoint) so that this sequence succeeds without additional manual setup.
|
||||||
78
generation/scaffolding-rules.md
Normal file
78
generation/scaffolding-rules.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Project Scaffolding Rules
|
||||||
|
|
||||||
|
The generator must use **official CLI tools** to create base project structures.
|
||||||
|
|
||||||
|
The AI must **not** manually generate the entire project skeleton (e.g. by writing all config files and folder structure by hand). Using the CLI reduces errors and ensures compatibility with current tool versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backend Scaffolding
|
||||||
|
|
||||||
|
Use **NestJS CLI**.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @nestjs/cli@10.3.2 new server --package-manager npm --skip-git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Project directory** must be `server`.
|
||||||
|
- **TypeScript** must be used (default for Nest CLI).
|
||||||
|
- **npm** must be the package manager (`--package-manager npm`).
|
||||||
|
- **Git** initialization must be skipped (`--skip-git`).
|
||||||
|
|
||||||
|
## After scaffolding — install required dependencies
|
||||||
|
|
||||||
|
Run from the `server` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @prisma/client
|
||||||
|
npm install prisma --save-dev
|
||||||
|
npm install @nestjs/config
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frontend Scaffolding
|
||||||
|
|
||||||
|
Use **Vite CLI**.
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create vite@5.2.0 client -- --template react-ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Project directory** must be `client`.
|
||||||
|
- **React + TypeScript** template must be used (`--template react-ts`).
|
||||||
|
|
||||||
|
## After scaffolding — install required dependencies
|
||||||
|
|
||||||
|
Run from the `client` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-admin
|
||||||
|
npm install ra-data-simple-rest
|
||||||
|
npm install @mui/material @emotion/react @emotion/styled
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scaffolding Strategy
|
||||||
|
|
||||||
|
Generation pipeline order:
|
||||||
|
|
||||||
|
1. **Parse DSL** — Read domain, DTO, API, and UI DSL files.
|
||||||
|
2. **Run CLI scaffolding** — Create `server` with NestJS CLI and `client` with Vite CLI; install dependencies as above.
|
||||||
|
3. **Code generation** — Generate Prisma schema, NestJS modules/DTOs/PrismaService, and React Admin resources.
|
||||||
|
4. **Runtime infrastructure** — Generate `.env`, `.env.example`, package lifecycle scripts, and runtime config files.
|
||||||
|
5. **Database runtime** — Generate `docker-compose.yml` in project root with PostgreSQL service (`postgres`, image `postgres:16`, port `5432:5432`).
|
||||||
|
6. **Migration** — Apply schema with `npx prisma migrate dev`.
|
||||||
|
7. **Seed** — Populate minimal development data with `npx prisma db seed`.
|
||||||
|
8. **Validation** — Run checks from `generation/post-generation-validation.md`.
|
||||||
|
|
||||||
|
Scaffolding (steps 1–2) must be done with the CLI; steps 3–8 are generated from the DSL and project docs.
|
||||||
8
generation/update-strategy.md
Normal file
8
generation/update-strategy.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Update Strategy
|
||||||
|
|
||||||
|
When DSL changes:
|
||||||
|
|
||||||
|
1. Regenerate prisma.schema
|
||||||
|
2. Run prisma migrate dev
|
||||||
|
3. Regenerate Nest modules
|
||||||
|
4. Regenerate React Admin resources
|
||||||
1
server/.env.example
Normal file
1
server/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"
|
||||||
25
server/.eslintrc.js
Normal file
25
server/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
32
server/.gitignore
vendored
Normal file
32
server/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
prisma/migrations/**/migration_lock.toml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
4
server/.prettierrc
Normal file
4
server/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
99
server/README.md
Normal file
99
server/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
8
server/nest-cli.json
Normal file
8
server/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
9805
server/package-lock.json
generated
Normal file
9805
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
76
server/package.json
Normal file
76
server/package.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"postinstall": "prisma generate"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^4.0.3",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@prisma/client": "^5.22.0",
|
||||||
|
"reflect-metadata": "^0.2.0",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"prisma": "^5.22.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
74
server/prisma/schema.prisma
Normal file
74
server/prisma/schema.prisma
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EquipmentStatus {
|
||||||
|
Active
|
||||||
|
Repair
|
||||||
|
Reserve
|
||||||
|
WriteOff
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepairKind {
|
||||||
|
TO
|
||||||
|
TR
|
||||||
|
TRE
|
||||||
|
KR
|
||||||
|
AR
|
||||||
|
MP
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepairOrderStatus {
|
||||||
|
Draft
|
||||||
|
Approved
|
||||||
|
InWork
|
||||||
|
Done
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
model EquipmentType {
|
||||||
|
code String @id
|
||||||
|
name String
|
||||||
|
manufacturer String?
|
||||||
|
maintenanceIntervalHours Int?
|
||||||
|
overhaulIntervalHours Int?
|
||||||
|
equipment Equipment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Equipment {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
inventoryNumber String @unique
|
||||||
|
serialNumber String?
|
||||||
|
name String
|
||||||
|
equipmentTypeCode String
|
||||||
|
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
|
||||||
|
status EquipmentStatus @default(Active)
|
||||||
|
location String?
|
||||||
|
commissionedAt DateTime?
|
||||||
|
totalEngineHours Decimal?
|
||||||
|
engineHoursSinceLastRepair Decimal?
|
||||||
|
lastRepairAt DateTime?
|
||||||
|
notes String?
|
||||||
|
repairOrders RepairOrder[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model RepairOrder {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
number String @unique
|
||||||
|
equipmentId String
|
||||||
|
equipment Equipment @relation(fields: [equipmentId], references: [id])
|
||||||
|
repairKind RepairKind
|
||||||
|
status RepairOrderStatus @default(Draft)
|
||||||
|
plannedAt DateTime
|
||||||
|
startedAt DateTime?
|
||||||
|
completedAt DateTime?
|
||||||
|
contractor String?
|
||||||
|
engineHoursAtRepair Decimal?
|
||||||
|
description String?
|
||||||
|
notes String?
|
||||||
|
}
|
||||||
100
server/prisma/seed.ts
Normal file
100
server/prisma/seed.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const equipmentType = await prisma.equipmentType.upsert({
|
||||||
|
where: { code: 'pump' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
code: 'pump',
|
||||||
|
name: 'Насосный агрегат',
|
||||||
|
manufacturer: 'АО НасосПром',
|
||||||
|
maintenanceIntervalHours: 2000,
|
||||||
|
overhaulIntervalHours: 16000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipmentType2 = await prisma.equipmentType.upsert({
|
||||||
|
where: { code: 'compressor' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
code: 'compressor',
|
||||||
|
name: 'Компрессорная установка',
|
||||||
|
manufacturer: 'ОАО Компрессормаш',
|
||||||
|
maintenanceIntervalHours: 1500,
|
||||||
|
overhaulIntervalHours: 12000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipment = await prisma.equipment.upsert({
|
||||||
|
where: { inventoryNumber: 'INV-001' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
inventoryNumber: 'INV-001',
|
||||||
|
serialNumber: 'SN-2024-0001',
|
||||||
|
name: 'Насос ЦНС 180-212',
|
||||||
|
equipmentTypeCode: 'pump',
|
||||||
|
status: 'Active',
|
||||||
|
location: 'Куст №5, скважина 42',
|
||||||
|
commissionedAt: new Date('2023-06-15'),
|
||||||
|
totalEngineHours: 4500,
|
||||||
|
engineHoursSinceLastRepair: 1200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipment2 = await prisma.equipment.upsert({
|
||||||
|
where: { inventoryNumber: 'INV-002' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
inventoryNumber: 'INV-002',
|
||||||
|
serialNumber: 'SN-2024-0002',
|
||||||
|
name: 'Компрессор 4ВМ10-120/9',
|
||||||
|
equipmentTypeCode: 'compressor',
|
||||||
|
status: 'Active',
|
||||||
|
location: 'ГКС-3',
|
||||||
|
commissionedAt: new Date('2022-03-10'),
|
||||||
|
totalEngineHours: 8200,
|
||||||
|
engineHoursSinceLastRepair: 800,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.repairOrder.upsert({
|
||||||
|
where: { number: 'RO-2026-001' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
number: 'RO-2026-001',
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
repairKind: 'TO',
|
||||||
|
status: 'Approved',
|
||||||
|
plannedAt: new Date('2026-04-01'),
|
||||||
|
contractor: 'ООО СервисРемонт',
|
||||||
|
engineHoursAtRepair: 4500,
|
||||||
|
description: 'Плановое техническое обслуживание насосного агрегата',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.repairOrder.upsert({
|
||||||
|
where: { number: 'RO-2026-002' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
number: 'RO-2026-002',
|
||||||
|
equipmentId: equipment2.id,
|
||||||
|
repairKind: 'TR',
|
||||||
|
status: 'Draft',
|
||||||
|
plannedAt: new Date('2026-05-15'),
|
||||||
|
description: 'Текущий ремонт компрессорной установки',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Seed data created successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
19
server/src/app.module.ts
Normal file
19
server/src/app.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { EquipmentTypeModule } from './modules/equipment-type/equipment-type.module';
|
||||||
|
import { EquipmentModule } from './modules/equipment/equipment.module';
|
||||||
|
import { RepairOrderModule } from './modules/repair-order/repair-order.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
PrismaModule,
|
||||||
|
HealthModule,
|
||||||
|
EquipmentTypeModule,
|
||||||
|
EquipmentModule,
|
||||||
|
RepairOrderModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
9
server/src/health/health.controller.ts
Normal file
9
server/src/health/health.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('health')
|
||||||
|
export class HealthController {
|
||||||
|
@Get()
|
||||||
|
getHealth() {
|
||||||
|
return { status: 'ok' };
|
||||||
|
}
|
||||||
|
}
|
||||||
7
server/src/health/health.module.ts
Normal file
7
server/src/health/health.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [HealthController],
|
||||||
|
})
|
||||||
|
export class HealthModule {}
|
||||||
12
server/src/main.ts
Normal file
12
server/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors({
|
||||||
|
origin: true,
|
||||||
|
exposedHeaders: ['Content-Range'],
|
||||||
|
});
|
||||||
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class CreateEquipmentTypeDto {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
maintenanceIntervalHours?: number;
|
||||||
|
overhaulIntervalHours?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export class UpdateEquipmentTypeDto {
|
||||||
|
name?: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
maintenanceIntervalHours?: number;
|
||||||
|
overhaulIntervalHours?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { EquipmentTypeService } from './equipment-type.service';
|
||||||
|
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
|
||||||
|
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
|
||||||
|
|
||||||
|
@Controller('equipment-types')
|
||||||
|
export class EquipmentTypeController {
|
||||||
|
constructor(private readonly equipmentTypeService: EquipmentTypeService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
|
const result = await this.equipmentTypeService.findAll(query);
|
||||||
|
res.set('Content-Range', `equipment-types ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
|
return res.json(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':code')
|
||||||
|
findOne(@Param('code') code: string) {
|
||||||
|
return this.equipmentTypeService.findOne(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() dto: CreateEquipmentTypeDto) {
|
||||||
|
return this.equipmentTypeService.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':code')
|
||||||
|
update(@Param('code') code: string, @Body() dto: UpdateEquipmentTypeDto) {
|
||||||
|
return this.equipmentTypeService.update(code, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':code')
|
||||||
|
remove(@Param('code') code: string) {
|
||||||
|
return this.equipmentTypeService.remove(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { EquipmentTypeController } from './equipment-type.controller';
|
||||||
|
import { EquipmentTypeService } from './equipment-type.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [EquipmentTypeController],
|
||||||
|
providers: [EquipmentTypeService],
|
||||||
|
})
|
||||||
|
export class EquipmentTypeModule {}
|
||||||
78
server/src/modules/equipment-type/equipment-type.service.ts
Normal file
78
server/src/modules/equipment-type/equipment-type.service.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
|
||||||
|
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EquipmentTypeService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll(query: {
|
||||||
|
_start?: string;
|
||||||
|
_end?: string;
|
||||||
|
_sort?: string;
|
||||||
|
_order?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) {
|
||||||
|
const start = parseInt(query._start) || 0;
|
||||||
|
const end = parseInt(query._end) || 10;
|
||||||
|
const take = end - start;
|
||||||
|
const skip = start;
|
||||||
|
const sortField = 'code';
|
||||||
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (query.code) where.code = { contains: query.code, mode: 'insensitive' };
|
||||||
|
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
||||||
|
if (query.manufacturer)
|
||||||
|
where.manufacturer = {
|
||||||
|
contains: query.manufacturer,
|
||||||
|
mode: 'insensitive',
|
||||||
|
};
|
||||||
|
if (query.id) {
|
||||||
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
|
where.code = { in: ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
this.prisma.equipmentType.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
orderBy: { [sortField]: sortOrder },
|
||||||
|
}),
|
||||||
|
this.prisma.equipmentType.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((item) => ({ id: item.code, ...item })),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(code: string) {
|
||||||
|
const record = await this.prisma.equipmentType.findUniqueOrThrow({
|
||||||
|
where: { code },
|
||||||
|
});
|
||||||
|
return { id: record.code, ...record };
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateEquipmentTypeDto) {
|
||||||
|
const record = await this.prisma.equipmentType.create({ data: dto });
|
||||||
|
return { id: record.code, ...record };
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(code: string, dto: UpdateEquipmentTypeDto) {
|
||||||
|
const { id, code: _pk, ...data } = dto as any;
|
||||||
|
const record = await this.prisma.equipmentType.update({
|
||||||
|
where: { code },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return { id: record.code, ...record };
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(code: string) {
|
||||||
|
const record = await this.prisma.equipmentType.delete({ where: { code } });
|
||||||
|
return { id: record.code, ...record };
|
||||||
|
}
|
||||||
|
}
|
||||||
13
server/src/modules/equipment/dto/create-equipment.dto.ts
Normal file
13
server/src/modules/equipment/dto/create-equipment.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class CreateEquipmentDto {
|
||||||
|
inventoryNumber: string;
|
||||||
|
serialNumber?: string;
|
||||||
|
name: string;
|
||||||
|
equipmentTypeCode: string;
|
||||||
|
status?: string;
|
||||||
|
location?: string;
|
||||||
|
commissionedAt?: string;
|
||||||
|
totalEngineHours?: string;
|
||||||
|
engineHoursSinceLastRepair?: string;
|
||||||
|
lastRepairAt?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
13
server/src/modules/equipment/dto/update-equipment.dto.ts
Normal file
13
server/src/modules/equipment/dto/update-equipment.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class UpdateEquipmentDto {
|
||||||
|
inventoryNumber?: string;
|
||||||
|
serialNumber?: string;
|
||||||
|
name?: string;
|
||||||
|
equipmentTypeCode?: string;
|
||||||
|
status?: string;
|
||||||
|
location?: string;
|
||||||
|
commissionedAt?: string;
|
||||||
|
totalEngineHours?: string;
|
||||||
|
engineHoursSinceLastRepair?: string;
|
||||||
|
lastRepairAt?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
38
server/src/modules/equipment/equipment.controller.ts
Normal file
38
server/src/modules/equipment/equipment.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { EquipmentService } from './equipment.service';
|
||||||
|
import { CreateEquipmentDto } from './dto/create-equipment.dto';
|
||||||
|
import { UpdateEquipmentDto } from './dto/update-equipment.dto';
|
||||||
|
|
||||||
|
@Controller('equipment')
|
||||||
|
export class EquipmentController {
|
||||||
|
constructor(private readonly equipmentService: EquipmentService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
|
const result = await this.equipmentService.findAll(query);
|
||||||
|
res.set('Content-Range', `equipment ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
|
return res.json(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.equipmentService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() dto: CreateEquipmentDto) {
|
||||||
|
return this.equipmentService.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() dto: UpdateEquipmentDto) {
|
||||||
|
return this.equipmentService.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.equipmentService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
server/src/modules/equipment/equipment.module.ts
Normal file
9
server/src/modules/equipment/equipment.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { EquipmentController } from './equipment.controller';
|
||||||
|
import { EquipmentService } from './equipment.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [EquipmentController],
|
||||||
|
providers: [EquipmentService],
|
||||||
|
})
|
||||||
|
export class EquipmentModule {}
|
||||||
88
server/src/modules/equipment/equipment.service.ts
Normal file
88
server/src/modules/equipment/equipment.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
import { CreateEquipmentDto } from './dto/create-equipment.dto';
|
||||||
|
import { UpdateEquipmentDto } from './dto/update-equipment.dto';
|
||||||
|
|
||||||
|
function serializeRecord(record: any) {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
totalEngineHours: record.totalEngineHours?.toString() ?? null,
|
||||||
|
engineHoursSinceLastRepair: record.engineHoursSinceLastRepair?.toString() ?? null,
|
||||||
|
commissionedAt: record.commissionedAt?.toISOString() ?? null,
|
||||||
|
lastRepairAt: record.lastRepairAt?.toISOString() ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EquipmentService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {
|
||||||
|
const start = parseInt(query._start) || 0;
|
||||||
|
const end = parseInt(query._end) || 10;
|
||||||
|
const take = end - start;
|
||||||
|
const skip = start;
|
||||||
|
const sortField = query._sort || 'id';
|
||||||
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (query.inventoryNumber) where.inventoryNumber = { contains: query.inventoryNumber, mode: 'insensitive' };
|
||||||
|
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
||||||
|
if (query.equipmentTypeCode) where.equipmentTypeCode = query.equipmentTypeCode;
|
||||||
|
if (query.status) where.status = query.status;
|
||||||
|
if (query.location) where.location = { contains: query.location, mode: 'insensitive' };
|
||||||
|
if (query.id) {
|
||||||
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
|
where.id = { in: ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
this.prisma.equipment.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
orderBy: { [sortField]: sortOrder },
|
||||||
|
}),
|
||||||
|
this.prisma.equipment.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map(serializeRecord),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const record = await this.prisma.equipment.findUniqueOrThrow({ where: { id } });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateEquipmentDto) {
|
||||||
|
const data: any = { ...dto };
|
||||||
|
if (dto.commissionedAt) data.commissionedAt = new Date(dto.commissionedAt);
|
||||||
|
if (dto.lastRepairAt) data.lastRepairAt = new Date(dto.lastRepairAt);
|
||||||
|
if (dto.totalEngineHours) data.totalEngineHours = new Prisma.Decimal(dto.totalEngineHours);
|
||||||
|
if (dto.engineHoursSinceLastRepair) data.engineHoursSinceLastRepair = new Prisma.Decimal(dto.engineHoursSinceLastRepair);
|
||||||
|
|
||||||
|
const record = await this.prisma.equipment.create({ data });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateEquipmentDto) {
|
||||||
|
const { id: _pk, ...rest } = dto as any;
|
||||||
|
const data: any = { ...rest };
|
||||||
|
if (data.commissionedAt) data.commissionedAt = new Date(data.commissionedAt);
|
||||||
|
if (data.lastRepairAt) data.lastRepairAt = new Date(data.lastRepairAt);
|
||||||
|
if (data.totalEngineHours !== undefined && data.totalEngineHours !== null) data.totalEngineHours = new Prisma.Decimal(data.totalEngineHours);
|
||||||
|
if (data.engineHoursSinceLastRepair !== undefined && data.engineHoursSinceLastRepair !== null) data.engineHoursSinceLastRepair = new Prisma.Decimal(data.engineHoursSinceLastRepair);
|
||||||
|
|
||||||
|
const record = await this.prisma.equipment.update({ where: { id }, data });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const record = await this.prisma.equipment.delete({ where: { id } });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export class CreateRepairOrderDto {
|
||||||
|
number: string;
|
||||||
|
equipmentId: string;
|
||||||
|
repairKind: string;
|
||||||
|
status?: string;
|
||||||
|
plannedAt: string;
|
||||||
|
startedAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
contractor?: string;
|
||||||
|
engineHoursAtRepair?: string;
|
||||||
|
description?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export class UpdateRepairOrderDto {
|
||||||
|
number?: string;
|
||||||
|
equipmentId?: string;
|
||||||
|
repairKind?: string;
|
||||||
|
status?: string;
|
||||||
|
plannedAt?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
contractor?: string;
|
||||||
|
engineHoursAtRepair?: string;
|
||||||
|
description?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
38
server/src/modules/repair-order/repair-order.controller.ts
Normal file
38
server/src/modules/repair-order/repair-order.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { RepairOrderService } from './repair-order.service';
|
||||||
|
import { CreateRepairOrderDto } from './dto/create-repair-order.dto';
|
||||||
|
import { UpdateRepairOrderDto } from './dto/update-repair-order.dto';
|
||||||
|
|
||||||
|
@Controller('repair-orders')
|
||||||
|
export class RepairOrderController {
|
||||||
|
constructor(private readonly repairOrderService: RepairOrderService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
|
const result = await this.repairOrderService.findAll(query);
|
||||||
|
res.set('Content-Range', `repair-orders ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
|
return res.json(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.repairOrderService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() dto: CreateRepairOrderDto) {
|
||||||
|
return this.repairOrderService.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() dto: UpdateRepairOrderDto) {
|
||||||
|
return this.repairOrderService.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.repairOrderService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
server/src/modules/repair-order/repair-order.module.ts
Normal file
9
server/src/modules/repair-order/repair-order.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RepairOrderController } from './repair-order.controller';
|
||||||
|
import { RepairOrderService } from './repair-order.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [RepairOrderController],
|
||||||
|
providers: [RepairOrderService],
|
||||||
|
})
|
||||||
|
export class RepairOrderModule {}
|
||||||
88
server/src/modules/repair-order/repair-order.service.ts
Normal file
88
server/src/modules/repair-order/repair-order.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
import { CreateRepairOrderDto } from './dto/create-repair-order.dto';
|
||||||
|
import { UpdateRepairOrderDto } from './dto/update-repair-order.dto';
|
||||||
|
|
||||||
|
function serializeRecord(record: any) {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
engineHoursAtRepair: record.engineHoursAtRepair?.toString() ?? null,
|
||||||
|
plannedAt: record.plannedAt?.toISOString() ?? null,
|
||||||
|
startedAt: record.startedAt?.toISOString() ?? null,
|
||||||
|
completedAt: record.completedAt?.toISOString() ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RepairOrderService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {
|
||||||
|
const start = parseInt(query._start) || 0;
|
||||||
|
const end = parseInt(query._end) || 10;
|
||||||
|
const take = end - start;
|
||||||
|
const skip = start;
|
||||||
|
const sortField = query._sort || 'id';
|
||||||
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (query.number) where.number = { contains: query.number, mode: 'insensitive' };
|
||||||
|
if (query.equipmentId) where.equipmentId = query.equipmentId;
|
||||||
|
if (query.repairKind) where.repairKind = query.repairKind;
|
||||||
|
if (query.status) where.status = query.status;
|
||||||
|
if (query.contractor) where.contractor = { contains: query.contractor, mode: 'insensitive' };
|
||||||
|
if (query.id) {
|
||||||
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
|
where.id = { in: ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
this.prisma.repairOrder.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
orderBy: { [sortField]: sortOrder },
|
||||||
|
}),
|
||||||
|
this.prisma.repairOrder.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map(serializeRecord),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const record = await this.prisma.repairOrder.findUniqueOrThrow({ where: { id } });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateRepairOrderDto) {
|
||||||
|
const data: any = { ...dto };
|
||||||
|
if (dto.plannedAt) data.plannedAt = new Date(dto.plannedAt);
|
||||||
|
if (dto.startedAt) data.startedAt = new Date(dto.startedAt);
|
||||||
|
if (dto.completedAt) data.completedAt = new Date(dto.completedAt);
|
||||||
|
if (dto.engineHoursAtRepair) data.engineHoursAtRepair = new Prisma.Decimal(dto.engineHoursAtRepair);
|
||||||
|
|
||||||
|
const record = await this.prisma.repairOrder.create({ data });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateRepairOrderDto) {
|
||||||
|
const { id: _pk, ...rest } = dto as any;
|
||||||
|
const data: any = { ...rest };
|
||||||
|
if (data.plannedAt) data.plannedAt = new Date(data.plannedAt);
|
||||||
|
if (data.startedAt) data.startedAt = new Date(data.startedAt);
|
||||||
|
if (data.completedAt) data.completedAt = new Date(data.completedAt);
|
||||||
|
if (data.engineHoursAtRepair !== undefined && data.engineHoursAtRepair !== null) data.engineHoursAtRepair = new Prisma.Decimal(data.engineHoursAtRepair);
|
||||||
|
|
||||||
|
const record = await this.prisma.repairOrder.update({ where: { id }, data });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const record = await this.prisma.repairOrder.delete({ where: { id } });
|
||||||
|
return serializeRecord(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
server/src/prisma/prisma.module.ts
Normal file
9
server/src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
9
server/src/prisma/prisma.service.ts
Normal file
9
server/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
server/test/app.e2e-spec.ts
Normal file
24
server/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
server/test/jest-e2e.json
Normal file
9
server/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
server/tsconfig.build.json
Normal file
4
server/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
server/tsconfig.json
Normal file
21
server/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user