first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
data/*.db
|
||||||
|
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
|
||||||
|
This repository contains a web application for creating, editing, calculating, and documenting electrical power balances for building-services electrical planning.
|
||||||
|
|
||||||
|
The application is intended for small internal use by approximately 2–3 concurrent users. It should support practical planning workflows, not an over-engineered enterprise architecture.
|
||||||
|
|
||||||
|
The domain is electrical building planning, especially German TGA / ELT planning. Important concepts include:
|
||||||
|
|
||||||
|
- Power balance / Leistungsbilanz
|
||||||
|
- Distribution boards / Verteilungen
|
||||||
|
- Electrical consumers / Verbraucher
|
||||||
|
- Installed power / installierte Leistung
|
||||||
|
- Demand factor / Gleichzeitigkeitsfaktor
|
||||||
|
- Calculated demand power / berechnete Leistung
|
||||||
|
- Voltage, current, phases, cos phi
|
||||||
|
- Device groups and individual devices
|
||||||
|
- Project-based calculation and documentation
|
||||||
|
|
||||||
|
Use English identifiers in code, database fields, interfaces, classes, and file names. German wording is allowed and preferred in UI labels, reports, and domain-facing text.
|
||||||
|
|
||||||
|
## Main Goal
|
||||||
|
|
||||||
|
Build a maintainable web application that allows users to:
|
||||||
|
|
||||||
|
- Create and manage projects
|
||||||
|
- Define electrical distributions or calculation areas
|
||||||
|
- Add individual electrical consumers
|
||||||
|
- Add grouped consumers with quantity
|
||||||
|
- Assign technical parameters to consumers
|
||||||
|
- Calculate installed and demand power
|
||||||
|
- Structure consumers by project, distribution, area, system, or category
|
||||||
|
- Export or print a usable power-balance report later
|
||||||
|
|
||||||
|
The application should be practical for electrical planners and should keep the data model understandable.
|
||||||
|
|
||||||
|
## Technology Direction
|
||||||
|
|
||||||
|
Preferred stack:
|
||||||
|
|
||||||
|
- TypeScript
|
||||||
|
- Node.js backend
|
||||||
|
- SQLite database for the initial version
|
||||||
|
- ORM is allowed and preferred if it improves type safety and maintainability
|
||||||
|
- Drizzle ORM is preferred for a more explicit SQL-oriented approach
|
||||||
|
- Prisma is acceptable if the project already uses it
|
||||||
|
- Frontend may be React-based if a UI is implemented
|
||||||
|
- Docker support for local development is desired
|
||||||
|
|
||||||
|
Do not introduce unnecessary infrastructure such as PostgreSQL, Redis, Kubernetes, microservices, message queues, or complex event systems unless explicitly requested.
|
||||||
|
|
||||||
|
SQLite is sufficient for the expected initial workload.
|
||||||
|
|
||||||
|
## Architecture Principles
|
||||||
|
|
||||||
|
Keep the application modular, but avoid excessive abstraction.
|
||||||
|
|
||||||
|
Preferred structure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
├─ domain/
|
||||||
|
│ ├─ models/
|
||||||
|
│ ├─ services/
|
||||||
|
│ └─ calculations/
|
||||||
|
├─ db/
|
||||||
|
│ ├─ schema/
|
||||||
|
│ ├─ migrations/
|
||||||
|
│ └─ repositories/
|
||||||
|
├─ server/
|
||||||
|
│ ├─ routes/
|
||||||
|
│ ├─ controllers/
|
||||||
|
│ └─ middleware/
|
||||||
|
├─ frontend/
|
||||||
|
│ ├─ components/
|
||||||
|
│ ├─ pages/
|
||||||
|
│ └─ utils/
|
||||||
|
└─ shared/
|
||||||
|
├─ types/
|
||||||
|
└─ validation/
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact structure may be simplified if the project is still small.
|
||||||
|
|
||||||
|
Use object-oriented design where it is useful for domain behavior, but do not force everything into classes.
|
||||||
|
|
||||||
|
Good candidates for classes:
|
||||||
|
|
||||||
|
- Project
|
||||||
|
- PowerBalance
|
||||||
|
- DistributionBoard
|
||||||
|
- Consumer
|
||||||
|
- ConsumerGroup
|
||||||
|
- CalculationService
|
||||||
|
- ReportGenerator
|
||||||
|
|
||||||
|
Good candidates for interfaces/types:
|
||||||
|
|
||||||
|
- DTOs
|
||||||
|
- API request/response shapes
|
||||||
|
- ORM result types
|
||||||
|
- Configuration objects
|
||||||
|
- Form data
|
||||||
|
- Validation schemas
|
||||||
|
|
||||||
|
Avoid duplicating the same model excessively across domain, database, API, and frontend. Some separation is acceptable, but keep mappings simple and explicit.
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
Use English names in code.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `Project`
|
||||||
|
- `PowerBalance`
|
||||||
|
- `DistributionBoard`
|
||||||
|
- `Consumer`
|
||||||
|
- `ConsumerGroup`
|
||||||
|
- `installedPower`
|
||||||
|
- `demandFactor`
|
||||||
|
- `demandPower`
|
||||||
|
- `ratedCurrent`
|
||||||
|
- `powerFactor`
|
||||||
|
- `quantity`
|
||||||
|
- `voltage`
|
||||||
|
- `phaseCount`
|
||||||
|
|
||||||
|
Use PascalCase for classes, interfaces, React components, and types.
|
||||||
|
|
||||||
|
Use camelCase for variables, properties, functions, and methods.
|
||||||
|
|
||||||
|
Use kebab-case for file names unless the local framework convention requires otherwise.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
power-balance.service.ts
|
||||||
|
consumer.model.ts
|
||||||
|
distribution-board.repository.ts
|
||||||
|
calculation-utils.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain Model Guidelines
|
||||||
|
|
||||||
|
A consumer must still have a quantity. This allows the same structure to represent either a single device or a grouped device entry.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Consumer = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId?: string;
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnit: number;
|
||||||
|
installedPowerTotal: number;
|
||||||
|
demandFactor: number;
|
||||||
|
demandPower: number;
|
||||||
|
voltage?: number;
|
||||||
|
phaseCount?: 1 | 3;
|
||||||
|
powerFactor?: number;
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The calculated fields may either be persisted or calculated dynamically. Prefer dynamic calculation first unless persistence is needed for reporting, auditability, or performance.
|
||||||
|
|
||||||
|
## Calculation Rules
|
||||||
|
|
||||||
|
Keep calculation logic centralized.
|
||||||
|
|
||||||
|
Do not spread electrical formulas across route handlers, UI components, or database repositories.
|
||||||
|
|
||||||
|
Use dedicated calculation services or pure functions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/domain/calculations/power-calculation.ts
|
||||||
|
src/domain/services/power-balance.service.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical calculation concepts:
|
||||||
|
|
||||||
|
- Installed power total = quantity × installed power per unit
|
||||||
|
- Demand power = installed power total × demand factor
|
||||||
|
- Current calculation depends on voltage, phase count, and power factor
|
||||||
|
|
||||||
|
When implementing formulas, add comments for the electrical meaning, especially where three-phase and single-phase calculations differ.
|
||||||
|
|
||||||
|
Always be careful with units.
|
||||||
|
|
||||||
|
Preferred base units:
|
||||||
|
|
||||||
|
- Power: kW
|
||||||
|
- Current: A
|
||||||
|
- Voltage: V
|
||||||
|
- Demand factor: decimal number from 0 to 1
|
||||||
|
|
||||||
|
If other units are added later, implement explicit conversion helpers.
|
||||||
|
|
||||||
|
## Database Guidelines
|
||||||
|
|
||||||
|
Use SQLite initially.
|
||||||
|
|
||||||
|
Keep schema clear and readable.
|
||||||
|
|
||||||
|
Avoid premature normalization. Normalize where it prevents real inconsistency, not just for theoretical purity.
|
||||||
|
|
||||||
|
Recommended core tables:
|
||||||
|
|
||||||
|
- projects
|
||||||
|
- power_balances
|
||||||
|
- distribution_boards
|
||||||
|
- consumers
|
||||||
|
- consumer_categories or system_types, if needed later
|
||||||
|
|
||||||
|
Use migrations. Do not manually modify the database schema without creating a migration.
|
||||||
|
|
||||||
|
Use repositories or database access modules. Do not place raw database queries directly in UI components or high-level route handlers.
|
||||||
|
|
||||||
|
## API Guidelines
|
||||||
|
|
||||||
|
Keep API routes resource-oriented.
|
||||||
|
|
||||||
|
Example routes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/projects
|
||||||
|
POST /api/projects
|
||||||
|
GET /api/projects/:projectId
|
||||||
|
PUT /api/projects/:projectId
|
||||||
|
DELETE /api/projects/:projectId
|
||||||
|
|
||||||
|
GET /api/projects/:projectId/power-balances
|
||||||
|
POST /api/projects/:projectId/power-balances
|
||||||
|
|
||||||
|
GET /api/power-balances/:powerBalanceId/consumers
|
||||||
|
POST /api/power-balances/:powerBalanceId/consumers
|
||||||
|
PUT /api/consumers/:consumerId
|
||||||
|
DELETE /api/consumers/:consumerId
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate all incoming API data.
|
||||||
|
|
||||||
|
Use shared validation schemas if possible.
|
||||||
|
|
||||||
|
## UI Guidelines
|
||||||
|
|
||||||
|
The UI should be practical and data-entry friendly.
|
||||||
|
|
||||||
|
Prioritize:
|
||||||
|
|
||||||
|
- Tables for consumers
|
||||||
|
- Inline editing where useful
|
||||||
|
- Clear grouping by distribution board or area
|
||||||
|
- Totals at group and project level
|
||||||
|
- German UI labels
|
||||||
|
- Consistent units in column headers
|
||||||
|
- Minimal clicks for adding multiple consumers
|
||||||
|
|
||||||
|
Use German labels such as:
|
||||||
|
|
||||||
|
- Projekt
|
||||||
|
- Leistungsbilanz
|
||||||
|
- Verteilung
|
||||||
|
- Verbraucher
|
||||||
|
- Anzahl
|
||||||
|
- Leistung je Stück
|
||||||
|
- Installierte Leistung
|
||||||
|
- Gleichzeitigkeitsfaktor
|
||||||
|
- Berechnete Leistung
|
||||||
|
- Bemerkung
|
||||||
|
|
||||||
|
Code identifiers must remain English.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
Add tests for calculation logic.
|
||||||
|
|
||||||
|
Calculation tests are more important than superficial UI tests.
|
||||||
|
|
||||||
|
At minimum, test:
|
||||||
|
|
||||||
|
- Total installed power calculation
|
||||||
|
- Demand power calculation
|
||||||
|
- Quantity handling
|
||||||
|
- Single-phase current calculation, if implemented
|
||||||
|
- Three-phase current calculation, if implemented
|
||||||
|
- Edge cases such as quantity 0, demand factor 0, and missing optional values
|
||||||
|
|
||||||
|
Do not change formulas without updating or adding tests.
|
||||||
|
|
||||||
|
## Docker and Development
|
||||||
|
|
||||||
|
The project should be runnable in a local development environment, including on Windows with Docker Desktop.
|
||||||
|
|
||||||
|
If Docker is used, provide:
|
||||||
|
|
||||||
|
- `Dockerfile`
|
||||||
|
- `docker-compose.yml`
|
||||||
|
- clear volume handling for SQLite database files
|
||||||
|
- documented development commands
|
||||||
|
|
||||||
|
Avoid configurations that only work on Linux unless explicitly documented.
|
||||||
|
|
||||||
|
## Documentation Expectations
|
||||||
|
|
||||||
|
Keep documentation short but useful.
|
||||||
|
|
||||||
|
Document:
|
||||||
|
|
||||||
|
- How to install dependencies
|
||||||
|
- How to start the dev server
|
||||||
|
- How to run migrations
|
||||||
|
- How to run tests
|
||||||
|
- Basic domain assumptions
|
||||||
|
- Important formulas
|
||||||
|
|
||||||
|
Prefer concise Markdown documentation.
|
||||||
|
|
||||||
|
## Coding Style
|
||||||
|
|
||||||
|
Write clear, explicit TypeScript.
|
||||||
|
|
||||||
|
Prefer readability over cleverness.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
- unnecessary generic abstractions
|
||||||
|
- magic numbers
|
||||||
|
- hidden side effects
|
||||||
|
- large files with unrelated responsibilities
|
||||||
|
- mixing UI, database, and calculation logic
|
||||||
|
|
||||||
|
Use meaningful names even if they are longer.
|
||||||
|
|
||||||
|
## Safety and Data Integrity
|
||||||
|
|
||||||
|
Do not delete user data without explicit confirmation in the UI or API design.
|
||||||
|
|
||||||
|
Use soft-delete only if the project introduces audit or recovery requirements.
|
||||||
|
|
||||||
|
Validate numeric values carefully:
|
||||||
|
|
||||||
|
- quantity must not be negative
|
||||||
|
- demand factor should normally be between 0 and 1
|
||||||
|
- installed power must not be negative
|
||||||
|
- voltage must be positive
|
||||||
|
- phase count should be limited to valid values
|
||||||
|
|
||||||
|
## Agent Behavior
|
||||||
|
|
||||||
|
When modifying this repository:
|
||||||
|
|
||||||
|
1. Inspect the existing structure before adding new files.
|
||||||
|
2. Reuse existing patterns unless they are clearly wrong.
|
||||||
|
3. Keep changes small and focused.
|
||||||
|
4. Do not introduce large framework changes without explicit instruction.
|
||||||
|
5. Do not silently change the database technology.
|
||||||
|
6. Do not silently change the ORM.
|
||||||
|
7. Keep domain terms consistent.
|
||||||
|
8. Add or update tests when changing calculation logic.
|
||||||
|
9. Update this `AGENTS.md` if a new recurring project rule is established.
|
||||||
|
10. Prefer practical implementation over theoretical perfection.
|
||||||
|
|
||||||
|
## Out of Scope Unless Explicitly Requested
|
||||||
|
|
||||||
|
Do not implement the following unless explicitly requested:
|
||||||
|
|
||||||
|
- Multi-tenant user management
|
||||||
|
- Role-based permissions
|
||||||
|
- Cloud deployment
|
||||||
|
- PostgreSQL migration
|
||||||
|
- Realtime collaboration
|
||||||
|
- Complex report designer
|
||||||
|
- Full BIM integration
|
||||||
|
- GAEB integration
|
||||||
|
- Revit integration
|
||||||
|
- Authentication providers
|
||||||
|
- Enterprise audit logging
|
||||||
|
|
||||||
|
## Preferred First Milestone
|
||||||
|
|
||||||
|
The first useful milestone should be:
|
||||||
|
|
||||||
|
- Create a project
|
||||||
|
- Create one power balance
|
||||||
|
- Add distribution boards
|
||||||
|
- Add consumers with quantity
|
||||||
|
- Calculate installed power and demand power
|
||||||
|
- Show totals in the UI
|
||||||
|
- Persist data in SQLite
|
||||||
|
- Provide basic tests for calculation logic
|
||||||
|
|
||||||
|
Do not start with advanced reporting before the core data and calculation workflow works.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Leistungsbilanz
|
||||||
|
|
||||||
|
TypeScript backend for electrical power-balance planning.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. `npm install`
|
||||||
|
2. `npm run db:generate`
|
||||||
|
3. `npm run db:migrate`
|
||||||
|
4. `npm run dev`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `npm run dev`: Start API server
|
||||||
|
- `npm run build`: TypeScript build
|
||||||
|
- `npm run test`: Calculation tests
|
||||||
|
- `npm run db:generate`: Generate migrations
|
||||||
|
- `npm run db:migrate`: Apply migrations
|
||||||
|
|
||||||
|
## Current API
|
||||||
|
|
||||||
|
- `GET /health`
|
||||||
|
- `GET /api/projects`
|
||||||
|
- `POST /api/projects`
|
||||||
|
- `GET /api/consumers/projects/:projectId`
|
||||||
|
- `POST /api/consumers`
|
||||||
|
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
# Context Dump – Requirements for Electrical Load Balance and Circuit List Web App
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
The application shall be a web app for creating electrical load balances and combined load balance / circuit lists for electrical planning.
|
||||||
|
|
||||||
|
The application shall primarily work in a tabular way.
|
||||||
|
|
||||||
|
## 2. Basic Structure
|
||||||
|
|
||||||
|
The application manages projects.
|
||||||
|
|
||||||
|
A project contains:
|
||||||
|
|
||||||
|
- Multiple distribution boards
|
||||||
|
- One project-specific device list
|
||||||
|
|
||||||
|
Each distribution board contains:
|
||||||
|
|
||||||
|
- Exactly one circuit list
|
||||||
|
|
||||||
|
Additionally, there is:
|
||||||
|
|
||||||
|
- One global device list
|
||||||
|
- One project-specific device list per project
|
||||||
|
|
||||||
|
Basic hierarchy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Project
|
||||||
|
├── ProjectDeviceList
|
||||||
|
└── DistributionBoard
|
||||||
|
└── CircuitList
|
||||||
|
└── CircuitEntries
|
||||||
|
|
||||||
|
Global
|
||||||
|
└── GlobalDeviceList
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Projects
|
||||||
|
|
||||||
|
A project is the top-level domain object.
|
||||||
|
|
||||||
|
A project can contain multiple distribution boards.
|
||||||
|
|
||||||
|
A project has one project-specific device list.
|
||||||
|
|
||||||
|
Devices from the project-specific device list can be used in the circuit lists of the distribution boards within that project.
|
||||||
|
|
||||||
|
### Project Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
Project
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- distributionBoards: List<DistributionBoard>
|
||||||
|
- projectDeviceList: DeviceList
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Distribution Boards
|
||||||
|
|
||||||
|
A distribution board belongs to exactly one project.
|
||||||
|
|
||||||
|
A distribution board has exactly one circuit list.
|
||||||
|
|
||||||
|
A project can contain multiple distribution boards.
|
||||||
|
|
||||||
|
Each circuit list is therefore assigned to one distribution board.
|
||||||
|
|
||||||
|
### DistributionBoard Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
DistributionBoard
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- circuitList: CircuitList
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Circuit Lists
|
||||||
|
|
||||||
|
A circuit list belongs to one distribution board.
|
||||||
|
|
||||||
|
The circuit list is table-based.
|
||||||
|
|
||||||
|
One row in the table is one circuit entry.
|
||||||
|
|
||||||
|
Multiple rows may reference the same circuit number.
|
||||||
|
|
||||||
|
This is required so that multiple different devices can be assigned to the same circuit.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Circuit 1 | Sockets | 6 pcs
|
||||||
|
Circuit 1 | Lights | 3 pcs
|
||||||
|
Circuit 1 | Fixed load | 1 pc
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that one circuit can be represented by multiple table rows.
|
||||||
|
|
||||||
|
### CircuitList Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
CircuitList
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- entries: List<CircuitEntry>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Circuit Entries
|
||||||
|
|
||||||
|
A circuit entry is one row in the circuit list.
|
||||||
|
|
||||||
|
A circuit entry can:
|
||||||
|
|
||||||
|
- Be linked to a device from the project-specific device list
|
||||||
|
- Exist without a device
|
||||||
|
- Exist without a device reference
|
||||||
|
- Be detached from the originally linked device
|
||||||
|
- Have its own description
|
||||||
|
- Have its own values
|
||||||
|
|
||||||
|
A circuit entry may be incomplete.
|
||||||
|
|
||||||
|
In principle, there are no required fields.
|
||||||
|
|
||||||
|
### CircuitEntry Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
CircuitEntry
|
||||||
|
- id
|
||||||
|
- circuitNumber
|
||||||
|
- description
|
||||||
|
- roomNumber
|
||||||
|
- roomName
|
||||||
|
- device
|
||||||
|
- isLinkedToDevice
|
||||||
|
- quantity
|
||||||
|
- unitPower
|
||||||
|
- simultaneityFactor
|
||||||
|
- cosPhi
|
||||||
|
- totalPower
|
||||||
|
- deviceType
|
||||||
|
- phaseType
|
||||||
|
- tradeOrCostGroup
|
||||||
|
- group
|
||||||
|
- protectionType
|
||||||
|
- protectionRatedCurrent
|
||||||
|
- protectionCharacteristic
|
||||||
|
- cableType
|
||||||
|
- cableCrossSection
|
||||||
|
- comment
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Circuit Entry Description
|
||||||
|
|
||||||
|
The `description` of a circuit entry is independent from:
|
||||||
|
|
||||||
|
- The device name
|
||||||
|
- The device display name
|
||||||
|
- The device link
|
||||||
|
|
||||||
|
Even if a circuit entry is linked to a device, the circuit entry description can always be overwritten manually.
|
||||||
|
|
||||||
|
There are therefore three different naming fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device.name
|
||||||
|
Device.displayName
|
||||||
|
CircuitEntry.description
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meaning
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device.name
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal or unique name of the device.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device.displayName
|
||||||
|
```
|
||||||
|
|
||||||
|
Default visible name of the device.
|
||||||
|
|
||||||
|
```text
|
||||||
|
CircuitEntry.description
|
||||||
|
```
|
||||||
|
|
||||||
|
Concrete description in the circuit list. This can be manually overwritten.
|
||||||
|
|
||||||
|
## 8. Devices
|
||||||
|
|
||||||
|
The application shall support configurable devices.
|
||||||
|
|
||||||
|
A device can represent either:
|
||||||
|
|
||||||
|
- A single device
|
||||||
|
- A collection/group of devices
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Single pendant light
|
||||||
|
- Socket group with six sockets
|
||||||
|
- Workplace connection group
|
||||||
|
- Fixed load
|
||||||
|
|
||||||
|
### Device Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- displayName
|
||||||
|
- deviceType
|
||||||
|
- phaseType
|
||||||
|
- quantity
|
||||||
|
- unitPower
|
||||||
|
- cosPhi
|
||||||
|
- tradeOrCostGroup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attribute Meaning
|
||||||
|
|
||||||
|
#### `name`
|
||||||
|
|
||||||
|
Internal or unique name.
|
||||||
|
|
||||||
|
Used to distinguish similar devices.
|
||||||
|
|
||||||
|
#### `displayName`
|
||||||
|
|
||||||
|
Default visible name.
|
||||||
|
|
||||||
|
Multiple devices may have the same display name.
|
||||||
|
|
||||||
|
#### `deviceType`
|
||||||
|
|
||||||
|
The type of device.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Light
|
||||||
|
- Socket
|
||||||
|
- Fixed connection
|
||||||
|
- Generic
|
||||||
|
- Other values to be defined later
|
||||||
|
|
||||||
|
#### `phaseType`
|
||||||
|
|
||||||
|
Defines whether the device is single-phase or three-phase.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Single-phase
|
||||||
|
- Three-phase
|
||||||
|
|
||||||
|
#### `quantity`
|
||||||
|
|
||||||
|
Default quantity of the device.
|
||||||
|
|
||||||
|
This allows a device to be used either as a single device or as a collection.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device: Socket group 6x
|
||||||
|
quantity: 6
|
||||||
|
unitPower: 100 W
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `unitPower`
|
||||||
|
|
||||||
|
Power of one individual device or element.
|
||||||
|
|
||||||
|
#### `cosPhi`
|
||||||
|
|
||||||
|
Power factor.
|
||||||
|
|
||||||
|
#### `tradeOrCostGroup`
|
||||||
|
|
||||||
|
Assignment to a trade or cost group.
|
||||||
|
|
||||||
|
## 9. Device Name and Display Name
|
||||||
|
|
||||||
|
A device needs both a `name` and a `displayName`.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
Two devices may look the same in the circuit list but still be internally different.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device 1:
|
||||||
|
name: Pendant light high occupancy
|
||||||
|
displayName: Pendant light
|
||||||
|
|
||||||
|
Device 2:
|
||||||
|
name: Pendant light low occupancy
|
||||||
|
displayName: Pendant light
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows internally different devices to be displayed with the same name in the circuit list.
|
||||||
|
|
||||||
|
## 10. Device Lists
|
||||||
|
|
||||||
|
There are two types of device lists:
|
||||||
|
|
||||||
|
1. Global device list
|
||||||
|
2. Project-specific device list
|
||||||
|
|
||||||
|
### Global Device List
|
||||||
|
|
||||||
|
The global device list contains generally reusable devices.
|
||||||
|
|
||||||
|
It is intended to be used across projects.
|
||||||
|
|
||||||
|
### Project-Specific Device List
|
||||||
|
|
||||||
|
The project-specific device list belongs to a specific project.
|
||||||
|
|
||||||
|
It contains devices that can be used within that project.
|
||||||
|
|
||||||
|
Devices from this list can be selected in the circuit lists of the project's distribution boards.
|
||||||
|
|
||||||
|
### DeviceList Attributes
|
||||||
|
|
||||||
|
```text
|
||||||
|
DeviceList
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- devices: List<Device>
|
||||||
|
```
|
||||||
|
|
||||||
|
Possible values for `type`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
global
|
||||||
|
project
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Copying Devices Between Device Lists
|
||||||
|
|
||||||
|
Devices shall be copyable between the global device list and project-specific device lists.
|
||||||
|
|
||||||
|
A device can be copied:
|
||||||
|
|
||||||
|
- From the global device list to a project-specific device list
|
||||||
|
- From a project-specific device list to the global device list
|
||||||
|
|
||||||
|
Copying means:
|
||||||
|
|
||||||
|
- A separate device is created in the target list
|
||||||
|
- Permanent synchronization between the original device and the copied device is not defined as a requirement
|
||||||
|
|
||||||
|
## 12. Device Link Between Device and Circuit Entry
|
||||||
|
|
||||||
|
A circuit entry can be linked to a device from the project-specific device list.
|
||||||
|
|
||||||
|
When the link is active, device values can be used in the circuit entry.
|
||||||
|
|
||||||
|
If the underlying device is changed, all still-linked circuit entries shall be updated.
|
||||||
|
|
||||||
|
The link can be enabled or disabled per circuit entry.
|
||||||
|
|
||||||
|
If a circuit entry is detached from the device:
|
||||||
|
|
||||||
|
- The circuit entry remains in place
|
||||||
|
- The circuit entry keeps its own values
|
||||||
|
- Future changes to the device no longer affect that circuit entry
|
||||||
|
|
||||||
|
A circuit entry may also exist without a device and without a device reference.
|
||||||
|
|
||||||
|
## 13. Quantity
|
||||||
|
|
||||||
|
There is a quantity on the device and a quantity on the circuit entry.
|
||||||
|
|
||||||
|
### Device Quantity
|
||||||
|
|
||||||
|
The device quantity describes the default scope of a device.
|
||||||
|
|
||||||
|
It allows devices to represent either single devices or collections.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device: Socket group 6x
|
||||||
|
quantity: 6
|
||||||
|
unitPower: 100 W
|
||||||
|
```
|
||||||
|
|
||||||
|
### Circuit Entry Quantity
|
||||||
|
|
||||||
|
The circuit entry quantity describes the concrete quantity used in the individual circuit entry.
|
||||||
|
|
||||||
|
It can be prefilled from the device quantity when a circuit entry is created from a device.
|
||||||
|
|
||||||
|
It can be changed in the circuit entry.
|
||||||
|
|
||||||
|
### Add Count
|
||||||
|
|
||||||
|
When adding a device to a circuit list, there shall be a separate value defining how many circuit entries shall be created.
|
||||||
|
|
||||||
|
Important distinction:
|
||||||
|
|
||||||
|
```text
|
||||||
|
addCount != quantity
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device: Workplace socket
|
||||||
|
Add count: 5
|
||||||
|
Quantity per entry: 6
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
5 circuit entries
|
||||||
|
Each circuit entry has quantity = 6
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. Total Power
|
||||||
|
|
||||||
|
The total power shall be calculated automatically.
|
||||||
|
|
||||||
|
The total power is an attribute of the circuit entry.
|
||||||
|
|
||||||
|
The calculation is based at least on:
|
||||||
|
|
||||||
|
- quantity
|
||||||
|
- unitPower
|
||||||
|
- simultaneityFactor
|
||||||
|
|
||||||
|
The exact calculation logic can be specified later.
|
||||||
|
|
||||||
|
## 15. Rooms
|
||||||
|
|
||||||
|
Rooms are represented by two fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
roomNumber
|
||||||
|
roomName
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16. Trade / Cost Group and Group
|
||||||
|
|
||||||
|
Entries shall be groupable by domain-specific criteria.
|
||||||
|
|
||||||
|
The planned fields are:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tradeOrCostGroup
|
||||||
|
group
|
||||||
|
```
|
||||||
|
|
||||||
|
These fields shall help treat similar elements together.
|
||||||
|
|
||||||
|
## 17. Table Functions
|
||||||
|
|
||||||
|
The circuit list shall support common table operations.
|
||||||
|
|
||||||
|
Defined operations are:
|
||||||
|
|
||||||
|
- Create entry
|
||||||
|
- Edit entry
|
||||||
|
- Delete entry
|
||||||
|
- Duplicate entry
|
||||||
|
- Copy entry to another circuit list
|
||||||
|
- Add device as entry
|
||||||
|
- Add device multiple times as entries
|
||||||
|
- Set quantity within an entry
|
||||||
|
|
||||||
|
General requirement:
|
||||||
|
|
||||||
|
- All common table manipulations shall be possible
|
||||||
|
|
||||||
|
Additional table functions may be specified later, for example:
|
||||||
|
|
||||||
|
- Sorting
|
||||||
|
- Filtering
|
||||||
|
- Reordering
|
||||||
|
- Multi-selection
|
||||||
|
- Bulk editing
|
||||||
|
|
||||||
|
## 18. Required Fields
|
||||||
|
|
||||||
|
In principle, there shall be no required fields.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- Entries may be incomplete
|
||||||
|
- A circuit entry may exist without a device
|
||||||
|
- A circuit entry may exist without a device reference
|
||||||
|
- Values can be completed later
|
||||||
|
|
||||||
|
## 19. Selection Lists
|
||||||
|
|
||||||
|
The application shall support fixed selection lists.
|
||||||
|
|
||||||
|
The exact selection lists will be defined later.
|
||||||
|
|
||||||
|
Current possible candidates based on the requirements are:
|
||||||
|
|
||||||
|
- deviceType
|
||||||
|
- phaseType
|
||||||
|
- tradeOrCostGroup
|
||||||
|
- group
|
||||||
|
- protectionType
|
||||||
|
- protectionCharacteristic
|
||||||
|
- cableType
|
||||||
|
- cableCrossSection
|
||||||
|
|
||||||
|
These lists are not yet finally specified.
|
||||||
|
|
||||||
|
## 20. Copying and Duplicating
|
||||||
|
|
||||||
|
Circuit entries shall be duplicatable.
|
||||||
|
|
||||||
|
Circuit entries shall be copyable to other circuit lists.
|
||||||
|
|
||||||
|
Devices shall be copyable between the global device list and project-specific device lists.
|
||||||
|
|
||||||
|
When adding a device to a circuit list, the user shall be able to define how many circuit entries shall be created from it.
|
||||||
|
|
||||||
|
Open point:
|
||||||
|
|
||||||
|
- Whether a device link is kept or detached when copying a circuit entry to another circuit list is not yet specified.
|
||||||
|
|
||||||
|
## 21. Simplified Class Model
|
||||||
|
|
||||||
|
The model shall use multiple classes but shall not be overly fragmented.
|
||||||
|
|
||||||
|
Current final class model:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Project
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- distributionBoards: List<DistributionBoard>
|
||||||
|
- projectDeviceList: DeviceList
|
||||||
|
|
||||||
|
DistributionBoard
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- circuitList: CircuitList
|
||||||
|
|
||||||
|
CircuitList
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- entries: List<CircuitEntry>
|
||||||
|
|
||||||
|
CircuitEntry
|
||||||
|
- id
|
||||||
|
- circuitNumber
|
||||||
|
- description
|
||||||
|
- roomNumber
|
||||||
|
- roomName
|
||||||
|
- device: Device
|
||||||
|
- isLinkedToDevice
|
||||||
|
- quantity
|
||||||
|
- unitPower
|
||||||
|
- simultaneityFactor
|
||||||
|
- cosPhi
|
||||||
|
- totalPower
|
||||||
|
- deviceType
|
||||||
|
- phaseType
|
||||||
|
- tradeOrCostGroup
|
||||||
|
- group
|
||||||
|
- protectionType
|
||||||
|
- protectionRatedCurrent
|
||||||
|
- protectionCharacteristic
|
||||||
|
- cableType
|
||||||
|
- cableCrossSection
|
||||||
|
- comment
|
||||||
|
|
||||||
|
Device
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- displayName
|
||||||
|
- deviceType
|
||||||
|
- phaseType
|
||||||
|
- quantity
|
||||||
|
- unitPower
|
||||||
|
- cosPhi
|
||||||
|
- tradeOrCostGroup
|
||||||
|
|
||||||
|
DeviceList
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- devices: List<Device>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 22. Central Domain Rules
|
||||||
|
|
||||||
|
1. A project contains multiple distribution boards.
|
||||||
|
|
||||||
|
2. Each distribution board contains exactly one circuit list.
|
||||||
|
|
||||||
|
3. A circuit list consists of circuit entries.
|
||||||
|
|
||||||
|
4. One row in the circuit list is one circuit entry.
|
||||||
|
|
||||||
|
5. Multiple circuit entries may have the same circuit number.
|
||||||
|
|
||||||
|
6. This allows multiple different devices to be assigned to one circuit.
|
||||||
|
|
||||||
|
7. A circuit entry can be linked to a device.
|
||||||
|
|
||||||
|
8. A circuit entry can exist without a device.
|
||||||
|
|
||||||
|
9. A circuit entry can exist without a device reference.
|
||||||
|
|
||||||
|
10. The link between a circuit entry and a device can be detached per entry.
|
||||||
|
|
||||||
|
11. If a device is changed, all still-linked circuit entries are updated.
|
||||||
|
|
||||||
|
12. Detached circuit entries are no longer changed by later device changes.
|
||||||
|
|
||||||
|
13. A device has a default quantity.
|
||||||
|
|
||||||
|
14. A circuit entry has its own quantity.
|
||||||
|
|
||||||
|
15. The quantity on the device can be used as a default value for new circuit entries.
|
||||||
|
|
||||||
|
16. The description of a circuit entry can be overwritten independently from the linked device.
|
||||||
|
|
||||||
|
17. The total power of a circuit entry is calculated automatically.
|
||||||
|
|
||||||
|
18. A project has a project-specific device list.
|
||||||
|
|
||||||
|
19. Additionally, there is a global device list.
|
||||||
|
|
||||||
|
20. Devices can be copied between the global device list and project-specific device lists.
|
||||||
|
|
||||||
|
21. Circuit entries can be duplicated.
|
||||||
|
|
||||||
|
22. Circuit entries can be copied to other circuit lists.
|
||||||
|
|
||||||
|
23. When adding a device, the user can define how many circuit entries shall be created.
|
||||||
|
|
||||||
|
24. In principle, there are no required fields.
|
||||||
|
|
||||||
|
25. The application shall support fixed selection lists. Details will be specified later.
|
||||||
|
|
||||||
|
## 23. Open Points
|
||||||
|
|
||||||
|
The following points are not yet finally specified:
|
||||||
|
|
||||||
|
1. Exact formula for calculating `totalPower`.
|
||||||
|
|
||||||
|
2. Exact list of fields synchronized while a circuit entry is linked to a device.
|
||||||
|
|
||||||
|
3. Whether the device link is kept or detached when copying a circuit entry to another circuit list.
|
||||||
|
|
||||||
|
4. Final selection lists and their values.
|
||||||
|
|
||||||
|
5. Exact behavior of table functions such as sorting, filtering, multi-selection, and bulk editing.
|
||||||
|
|
||||||
|
6. Whether global devices and project devices remain permanently independent after copying, or whether optional synchronization should be supported later.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const drizzle_kit_1 = require("drizzle-kit");
|
||||||
|
exports.default = (0, drizzle_kit_1.defineConfig)({
|
||||||
|
dialect: "sqlite",
|
||||||
|
schema: "./src/db/schema/*.ts",
|
||||||
|
out: "./src/db/migrations",
|
||||||
|
dbCredentials: {
|
||||||
|
url: "./data/leistungsbilanz.db",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: "sqlite",
|
||||||
|
schema: "./src/db/schema/*.ts",
|
||||||
|
out: "./src/db/migrations",
|
||||||
|
dbCredentials: {
|
||||||
|
url: "./data/leistungsbilanz.db",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Leistungsbilanz API</title>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="info">
|
||||||
|
<h1>Leistungsbilanz Backend</h1>
|
||||||
|
<p>Der aktuelle Stand laeuft als TypeScript Node/SQLite API.</p>
|
||||||
|
<p>Serverstart: <code>npm run dev</code></p>
|
||||||
|
<p>Healthcheck: <code>GET /health</code></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2842
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "leistungsbilanz",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/server/index.ts",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/server/index.js",
|
||||||
|
"test": "tsx --test tests/power-calculation.test.ts",
|
||||||
|
"test:watch": "tsx --watch --test tests/power-calculation.test.ts",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^12.9.0",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"zod": "^4.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
|
|
||||||
|
const dataDir = path.resolve("data");
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlite = new Database(path.resolve(dataDir, "leistungsbilanz.db"));
|
||||||
|
export const db = drizzle(sqlite);
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE `consumers` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`distribution_board_id` text,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`category` text,
|
||||||
|
`quantity` integer NOT NULL,
|
||||||
|
`installed_power_per_unit_kw` real NOT NULL,
|
||||||
|
`demand_factor` real NOT NULL,
|
||||||
|
`voltage_v` real,
|
||||||
|
`phase_count` integer,
|
||||||
|
`power_factor` real,
|
||||||
|
`note` text,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE set null
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `distribution_boards` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `projects` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "d219f155-9dd0-48e3-8fd2-8278f1f788ca",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"consumers": {
|
||||||
|
"name": "consumers",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distribution_board_id": {
|
||||||
|
"name": "distribution_board_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": "category",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"installed_power_per_unit_kw": {
|
||||||
|
"name": "installed_power_per_unit_kw",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"demand_factor": {
|
||||||
|
"name": "demand_factor",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"voltage_v": {
|
||||||
|
"name": "voltage_v",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"phase_count": {
|
||||||
|
"name": "phase_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"power_factor": {
|
||||||
|
"name": "power_factor",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"name": "note",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"consumers_project_id_projects_id_fk": {
|
||||||
|
"name": "consumers_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "consumers",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"consumers_distribution_board_id_distribution_boards_id_fk": {
|
||||||
|
"name": "consumers_distribution_board_id_distribution_boards_id_fk",
|
||||||
|
"tableFrom": "consumers",
|
||||||
|
"tableTo": "distribution_boards",
|
||||||
|
"columnsFrom": [
|
||||||
|
"distribution_board_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"distribution_boards": {
|
||||||
|
"name": "distribution_boards",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"distribution_boards_project_id_projects_id_fk": {
|
||||||
|
"name": "distribution_boards_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "distribution_boards",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777565414148,
|
||||||
|
"tag": "0000_bizarre_colossus",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { consumers } from "../schema/consumers.js";
|
||||||
|
import type { CreateConsumerInput } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
export class ConsumerRepository {
|
||||||
|
async listByProject(projectId: string) {
|
||||||
|
return db.select().from(consumers).where(eq(consumers.projectId, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(input: CreateConsumerInput) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
await db.insert(consumers).values({
|
||||||
|
id,
|
||||||
|
projectId: input.projectId,
|
||||||
|
distributionBoardId: input.distributionBoardId ?? null,
|
||||||
|
name: input.name,
|
||||||
|
category: input.category ?? null,
|
||||||
|
quantity: input.quantity,
|
||||||
|
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
|
||||||
|
demandFactor: input.demandFactor,
|
||||||
|
voltageV: input.voltageV ?? null,
|
||||||
|
phaseCount: input.phaseCount ?? null,
|
||||||
|
powerFactor: input.powerFactor ?? null,
|
||||||
|
note: input.note ?? null,
|
||||||
|
});
|
||||||
|
return { id, ...input };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "../client.js";
|
||||||
|
import { projects } from "../schema/projects.js";
|
||||||
|
|
||||||
|
export class ProjectRepository {
|
||||||
|
async list() {
|
||||||
|
return db.select().from(projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(name: string) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
await db.insert(projects).values({ id, name });
|
||||||
|
return { id, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(projectId: string) {
|
||||||
|
await db.delete(projects).where(eq(projects.id, projectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { distributionBoards } from "./distribution-boards.js";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const consumers = sqliteTable("consumers", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
category: text("category"),
|
||||||
|
quantity: integer("quantity").notNull(),
|
||||||
|
installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(),
|
||||||
|
demandFactor: real("demand_factor").notNull(),
|
||||||
|
voltageV: real("voltage_v"),
|
||||||
|
phaseCount: integer("phase_count"),
|
||||||
|
powerFactor: real("power_factor"),
|
||||||
|
note: text("note"),
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
export const distributionBoards = sqliteTable("distribution_boards", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export const projects = sqliteTable("projects", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export interface PowerInput {
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentInput {
|
||||||
|
demandPowerKw: number;
|
||||||
|
voltageV: number;
|
||||||
|
phaseCount: 1 | 3;
|
||||||
|
powerFactor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateInstalledPowerKw(input: PowerInput): number {
|
||||||
|
return input.quantity * input.installedPowerPerUnitKw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateDemandPowerKw(input: PowerInput): number {
|
||||||
|
return calculateInstalledPowerKw(input) * input.demandFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateCurrentA(input: CurrentInput): number {
|
||||||
|
const powerW = input.demandPowerKw * 1000;
|
||||||
|
if (input.phaseCount === 1) {
|
||||||
|
return powerW / (input.voltageV * input.powerFactor);
|
||||||
|
}
|
||||||
|
return powerW / (Math.sqrt(3) * input.voltageV * input.powerFactor);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export interface Consumer {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId?: string;
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV?: number;
|
||||||
|
phaseCount?: 1 | 3;
|
||||||
|
powerFactor?: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
calculateCurrentA,
|
||||||
|
calculateDemandPowerKw,
|
||||||
|
calculateInstalledPowerKw,
|
||||||
|
} from "../calculations/power-calculation.js";
|
||||||
|
import type { Consumer } from "../models/consumer.model.js";
|
||||||
|
|
||||||
|
export interface ConsumerWithCalculatedValues extends Consumer {
|
||||||
|
installedPowerKw: number;
|
||||||
|
demandPowerKw: number;
|
||||||
|
currentA?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PowerBalanceService {
|
||||||
|
enrichConsumer(consumer: Consumer): ConsumerWithCalculatedValues {
|
||||||
|
const installedPowerKw = calculateInstalledPowerKw({
|
||||||
|
quantity: consumer.quantity,
|
||||||
|
installedPowerPerUnitKw: consumer.installedPowerPerUnitKw,
|
||||||
|
demandFactor: consumer.demandFactor,
|
||||||
|
});
|
||||||
|
const demandPowerKw = calculateDemandPowerKw({
|
||||||
|
quantity: consumer.quantity,
|
||||||
|
installedPowerPerUnitKw: consumer.installedPowerPerUnitKw,
|
||||||
|
demandFactor: consumer.demandFactor,
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentA: number | undefined;
|
||||||
|
if (consumer.voltageV && consumer.phaseCount && consumer.powerFactor) {
|
||||||
|
currentA = calculateCurrentA({
|
||||||
|
demandPowerKw,
|
||||||
|
voltageV: consumer.voltageV,
|
||||||
|
phaseCount: consumer.phaseCount,
|
||||||
|
powerFactor: consumer.powerFactor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...consumer,
|
||||||
|
installedPowerKw,
|
||||||
|
demandPowerKw,
|
||||||
|
currentA,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Components Placeholder
|
||||||
|
|
||||||
|
Reusable UI components for project, distribution board, and consumer tables.
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Frontend Placeholder
|
||||||
|
|
||||||
|
Frontend implementation will move here in the next step (React/Next.js).
|
||||||
|
The backend API, domain logic, and SQLite persistence are now prepared.
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Frontend Utils Placeholder
|
||||||
|
|
||||||
|
Shared UI utility helpers will be implemented during the Next.js migration step.
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { ConsumerRepository } from "../../db/repositories/consumer.repository.js";
|
||||||
|
import { PowerBalanceService } from "../../domain/services/power-balance.service.js";
|
||||||
|
import { createConsumerSchema } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const consumerRepository = new ConsumerRepository();
|
||||||
|
const powerBalanceService = new PowerBalanceService();
|
||||||
|
|
||||||
|
export async function listConsumersByProject(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid projectId" });
|
||||||
|
}
|
||||||
|
const rows = await consumerRepository.listByProject(projectId);
|
||||||
|
const enriched = rows.map((row) =>
|
||||||
|
powerBalanceService.enrichConsumer({
|
||||||
|
id: row.id,
|
||||||
|
projectId: row.projectId,
|
||||||
|
distributionBoardId: row.distributionBoardId ?? undefined,
|
||||||
|
name: row.name,
|
||||||
|
category: row.category ?? undefined,
|
||||||
|
quantity: row.quantity,
|
||||||
|
installedPowerPerUnitKw: row.installedPowerPerUnitKw,
|
||||||
|
demandFactor: row.demandFactor,
|
||||||
|
voltageV: row.voltageV ?? undefined,
|
||||||
|
phaseCount: row.phaseCount === 1 || row.phaseCount === 3 ? row.phaseCount : undefined,
|
||||||
|
powerFactor: row.powerFactor ?? undefined,
|
||||||
|
note: row.note ?? undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.json(enriched);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createConsumer(req: Request, res: Response) {
|
||||||
|
const parsed = createConsumerSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
const created = await consumerRepository.create(parsed.data);
|
||||||
|
const enriched = powerBalanceService.enrichConsumer(created);
|
||||||
|
return res.status(201).json(enriched);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||||
|
import { createProjectSchema } from "../../shared/validation/consumer.schemas.js";
|
||||||
|
|
||||||
|
const projectRepository = new ProjectRepository();
|
||||||
|
|
||||||
|
export async function listProjects(_req: Request, res: Response) {
|
||||||
|
const result = await projectRepository.list();
|
||||||
|
res.json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProject(req: Request, res: Response) {
|
||||||
|
const parsed = createProjectSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
const project = await projectRepository.create(parsed.data.name);
|
||||||
|
return res.status(201).json(project);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { consumerRouter } from "./routes/consumer.routes.js";
|
||||||
|
import { projectRouter } from "./routes/project.routes.js";
|
||||||
|
import { errorMiddleware } from "./middleware/error.middleware.js";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = Number(process.env.PORT || 3000);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get("/health", (_req, res) => {
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/api/projects", projectRouter);
|
||||||
|
app.use("/api/consumers", consumerRouter);
|
||||||
|
|
||||||
|
app.use(errorMiddleware);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running on http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
|
||||||
|
export function errorMiddleware(
|
||||||
|
error: unknown,
|
||||||
|
_req: Request,
|
||||||
|
res: Response,
|
||||||
|
_next: NextFunction
|
||||||
|
) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { createConsumer, listConsumersByProject } from "../controllers/consumer.controller.js";
|
||||||
|
|
||||||
|
export const consumerRouter = Router();
|
||||||
|
|
||||||
|
consumerRouter.get("/projects/:projectId", listConsumersByProject);
|
||||||
|
consumerRouter.post("/", createConsumer);
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { createProject, listProjects } from "../controllers/project.controller.js";
|
||||||
|
|
||||||
|
export const projectRouter = Router();
|
||||||
|
|
||||||
|
projectRouter.get("/", listProjects);
|
||||||
|
projectRouter.post("/", createProject);
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export interface ProjectDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsumerDto {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
distributionBoardId: string | null;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: 1 | 3 | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const createConsumerSchema = z.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
distributionBoardId: z.string().min(1).optional(),
|
||||||
|
name: z.string().min(1),
|
||||||
|
category: z.string().optional(),
|
||||||
|
quantity: z.number().min(0),
|
||||||
|
installedPowerPerUnitKw: z.number().min(0),
|
||||||
|
demandFactor: z.number().min(0).max(1),
|
||||||
|
voltageV: z.number().positive().optional(),
|
||||||
|
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
|
||||||
|
powerFactor: z.number().min(0).max(1).optional(),
|
||||||
|
note: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createProjectSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateConsumerInput = z.infer<typeof createConsumerSchema>;
|
||||||
|
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
|
||||||
|
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const vitest_1 = require("vitest");
|
||||||
|
const power_calculation_js_1 = require("../src/domain/calculations/power-calculation.js");
|
||||||
|
(0, vitest_1.describe)("power calculation", () => {
|
||||||
|
(0, vitest_1.it)("calculates installed power", () => {
|
||||||
|
const result = (0, power_calculation_js_1.calculateInstalledPowerKw)({
|
||||||
|
quantity: 4,
|
||||||
|
installedPowerPerUnitKw: 1.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(result).toBeCloseTo(4.8);
|
||||||
|
});
|
||||||
|
(0, vitest_1.it)("calculates demand power", () => {
|
||||||
|
const result = (0, power_calculation_js_1.calculateDemandPowerKw)({
|
||||||
|
quantity: 4,
|
||||||
|
installedPowerPerUnitKw: 1.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(result).toBeCloseTo(3.84);
|
||||||
|
});
|
||||||
|
(0, vitest_1.it)("handles zero quantity", () => {
|
||||||
|
const result = (0, power_calculation_js_1.calculateInstalledPowerKw)({
|
||||||
|
quantity: 0,
|
||||||
|
installedPowerPerUnitKw: 3,
|
||||||
|
demandFactor: 0.9,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(result).toBe(0);
|
||||||
|
});
|
||||||
|
(0, vitest_1.it)("handles zero demand factor", () => {
|
||||||
|
const result = (0, power_calculation_js_1.calculateDemandPowerKw)({
|
||||||
|
quantity: 5,
|
||||||
|
installedPowerPerUnitKw: 2,
|
||||||
|
demandFactor: 0,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(result).toBe(0);
|
||||||
|
});
|
||||||
|
(0, vitest_1.it)("calculates single-phase current", () => {
|
||||||
|
const current = (0, power_calculation_js_1.calculateCurrentA)({
|
||||||
|
demandPowerKw: 2.3,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 0.95,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(current).toBeCloseTo(10.53, 2);
|
||||||
|
});
|
||||||
|
(0, vitest_1.it)("calculates three-phase current", () => {
|
||||||
|
const current = (0, power_calculation_js_1.calculateCurrentA)({
|
||||||
|
demandPowerKw: 9.5,
|
||||||
|
voltageV: 400,
|
||||||
|
phaseCount: 3,
|
||||||
|
powerFactor: 0.9,
|
||||||
|
});
|
||||||
|
(0, vitest_1.expect)(current).toBeCloseTo(15.23, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
calculateCurrentA,
|
||||||
|
calculateDemandPowerKw,
|
||||||
|
calculateInstalledPowerKw,
|
||||||
|
} from "../src/domain/calculations/power-calculation.js";
|
||||||
|
|
||||||
|
describe("power calculation", () => {
|
||||||
|
it("calculates installed power", () => {
|
||||||
|
const result = calculateInstalledPowerKw({
|
||||||
|
quantity: 4,
|
||||||
|
installedPowerPerUnitKw: 1.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
});
|
||||||
|
assert.ok(Math.abs(result - 4.8) < 0.00001);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates demand power", () => {
|
||||||
|
const result = calculateDemandPowerKw({
|
||||||
|
quantity: 4,
|
||||||
|
installedPowerPerUnitKw: 1.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
});
|
||||||
|
assert.ok(Math.abs(result - 3.84) < 0.00001);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles zero quantity", () => {
|
||||||
|
const result = calculateInstalledPowerKw({
|
||||||
|
quantity: 0,
|
||||||
|
installedPowerPerUnitKw: 3,
|
||||||
|
demandFactor: 0.9,
|
||||||
|
});
|
||||||
|
assert.equal(result, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles zero demand factor", () => {
|
||||||
|
const result = calculateDemandPowerKw({
|
||||||
|
quantity: 5,
|
||||||
|
installedPowerPerUnitKw: 2,
|
||||||
|
demandFactor: 0,
|
||||||
|
});
|
||||||
|
assert.equal(result, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates single-phase current", () => {
|
||||||
|
const current = calculateCurrentA({
|
||||||
|
demandPowerKw: 2.3,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 0.95,
|
||||||
|
});
|
||||||
|
assert.ok(Math.abs(current - 10.53) < 0.02);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates three-phase current", () => {
|
||||||
|
const current = calculateCurrentA({
|
||||||
|
demandPowerKw: 9.5,
|
||||||
|
voltageV: 400,
|
||||||
|
phaseCount: 3,
|
||||||
|
powerFactor: 0.9,
|
||||||
|
});
|
||||||
|
assert.ok(Math.abs(current - 15.23) < 0.02);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user