Initial commit: digital reception monorepo (M1-M11 + demo extensions)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PolimedController } from './polimed.controller';
|
||||
import { PolimedStore } from './polimed.store';
|
||||
|
||||
@Module({
|
||||
controllers: [PolimedController],
|
||||
providers: [PolimedStore],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
app.enableCors({ origin: true });
|
||||
const port = Number(process.env.POLIMED_MOCK_PORT ?? 4100);
|
||||
await app.listen(port);
|
||||
Logger.log(`polimed-mock listening on http://localhost:${port}`, 'Bootstrap');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post, Query } from '@nestjs/common';
|
||||
import { IsEnum, IsISO8601, IsOptional, IsString } from 'class-validator';
|
||||
import { PolimedStore } from './polimed.store';
|
||||
import type { VisitEventType } from './polimed.types';
|
||||
|
||||
class PushVisitEventDto {
|
||||
@IsEnum(['arrived', 'service_started', 'service_ended', 'left_without_service'])
|
||||
type!: VisitEventType;
|
||||
|
||||
@IsISO8601()
|
||||
occurredAt!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
source?: string;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class PolimedController {
|
||||
constructor(private readonly store: PolimedStore) {}
|
||||
|
||||
@Get('health')
|
||||
health() {
|
||||
return { status: 'ok', service: 'polimed-mock' };
|
||||
}
|
||||
|
||||
@Get('patients/search')
|
||||
searchPatients(@Query('q') q = '', @Query('limit') limit = '20') {
|
||||
return this.store.searchPatients(q, Number(limit) || 20);
|
||||
}
|
||||
|
||||
@Get('appointments')
|
||||
listAppointments(@Query('date') date?: string) {
|
||||
return this.store.listAppointments(date);
|
||||
}
|
||||
|
||||
@Get('appointments/:id')
|
||||
getAppointment(@Param('id') id: string) {
|
||||
return this.store.getAppointment(id);
|
||||
}
|
||||
|
||||
@Get('appointments/:id/events')
|
||||
getAppointmentEvents(@Param('id') id: string) {
|
||||
return this.store.getVisitEvents(id);
|
||||
}
|
||||
|
||||
@Post('visits/:appointmentId/events')
|
||||
@HttpCode(200)
|
||||
pushVisitEvent(@Param('appointmentId') appointmentId: string, @Body() body: PushVisitEventDto) {
|
||||
return this.store.pushVisitEvent(appointmentId, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Injectable, Logger, NotFoundException, OnModuleInit } from '@nestjs/common';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { PolimedAppointment, PolimedPatient, VisitEvent } from './polimed.types';
|
||||
|
||||
// В dev (nest start) __dirname = .../apps/polimed-mock/src — поднимаемся в корень app.
|
||||
// В prod (node dist/main.js) __dirname = .../apps/polimed-mock/dist — то же самое.
|
||||
const SEEDS_DIR = join(__dirname, '..', 'seeds');
|
||||
|
||||
@Injectable()
|
||||
export class PolimedStore implements OnModuleInit {
|
||||
private readonly logger = new Logger(PolimedStore.name);
|
||||
private patients: PolimedPatient[] = [];
|
||||
private appointments: PolimedAppointment[] = [];
|
||||
private visitEvents = new Map<string, VisitEvent[]>();
|
||||
|
||||
onModuleInit() {
|
||||
this.patients = JSON.parse(readFileSync(join(SEEDS_DIR, 'patients.json'), 'utf-8'));
|
||||
|
||||
const appointmentsTemplate: Array<Omit<PolimedAppointment, 'scheduledFor'> & { hourOffset: number; minuteOffset: number }> =
|
||||
JSON.parse(readFileSync(join(SEEDS_DIR, 'appointments.json'), 'utf-8'));
|
||||
|
||||
// Привязываем appointments к сегодняшней дате на старте — журнал «на сегодня» всегда живой.
|
||||
const today = new Date();
|
||||
today.setHours(8, 0, 0, 0);
|
||||
|
||||
this.appointments = appointmentsTemplate.map((a) => {
|
||||
const dt = new Date(today);
|
||||
dt.setHours(today.getHours() + a.hourOffset, today.getMinutes() + a.minuteOffset, 0, 0);
|
||||
return {
|
||||
id: a.id,
|
||||
patientId: a.patientId,
|
||||
patientFullName: a.patientFullName,
|
||||
doctorFullName: a.doctorFullName,
|
||||
specialty: a.specialty,
|
||||
status: a.status,
|
||||
scheduledFor: dt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Seeded ${this.patients.length} patients, ${this.appointments.length} appointments (today)`,
|
||||
);
|
||||
}
|
||||
|
||||
searchPatients(query: string, limit = 20): PolimedPatient[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return this.patients.slice(0, limit);
|
||||
return this.patients
|
||||
.filter(
|
||||
(p) =>
|
||||
p.fullName.toLowerCase().includes(q) ||
|
||||
p.cardNumber.toLowerCase().includes(q) ||
|
||||
p.phone.includes(q),
|
||||
)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
listAppointments(dateIso?: string): PolimedAppointment[] {
|
||||
if (!dateIso) return this.appointments;
|
||||
return this.appointments.filter((a) => a.scheduledFor.startsWith(dateIso));
|
||||
}
|
||||
|
||||
getAppointment(id: string): PolimedAppointment {
|
||||
const ap = this.appointments.find((a) => a.id === id);
|
||||
if (!ap) throw new NotFoundException(`Appointment ${id} not found`);
|
||||
return ap;
|
||||
}
|
||||
|
||||
pushVisitEvent(appointmentId: string, event: VisitEvent) {
|
||||
this.getAppointment(appointmentId); // throws 404 if missing
|
||||
const arr = this.visitEvents.get(appointmentId) ?? [];
|
||||
arr.push(event);
|
||||
this.visitEvents.set(appointmentId, arr);
|
||||
this.logger.log(
|
||||
`WRITE-BACK appointment=${appointmentId} event=${event.type} at=${event.occurredAt}`,
|
||||
);
|
||||
return { appointmentId, eventsTotal: arr.length };
|
||||
}
|
||||
|
||||
getVisitEvents(appointmentId: string): VisitEvent[] {
|
||||
return this.visitEvents.get(appointmentId) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface PolimedPatient {
|
||||
id: string;
|
||||
fullName: string;
|
||||
birthDate: string; // YYYY-MM-DD
|
||||
phone: string;
|
||||
cardNumber: string;
|
||||
}
|
||||
|
||||
export interface PolimedAppointment {
|
||||
id: string;
|
||||
patientId: string;
|
||||
patientFullName: string;
|
||||
doctorFullName: string;
|
||||
specialty: string;
|
||||
scheduledFor: string; // ISO datetime
|
||||
status: 'scheduled' | 'completed' | 'cancelled' | 'no_show';
|
||||
}
|
||||
|
||||
export type VisitEventType = 'arrived' | 'service_started' | 'service_ended' | 'left_without_service';
|
||||
|
||||
export interface VisitEvent {
|
||||
type: VisitEventType;
|
||||
occurredAt: string; // ISO datetime
|
||||
source?: string; // например 'reception-video'
|
||||
}
|
||||
Reference in New Issue
Block a user