feat(card1): версии тестов API, черновик, HR-login, import, UI
- V.1–V.3: saveTestDraft, fork при попытках; миграция 003 staff_id - V.4–V.6: REST /api/tests, activate, PATCH, start attempt - A: HR_DATABASE_URL + Werkzeug/bcrypt, JWT staffId, HR_AUTH - D.1: multipart /api/tests/import/document - Frontend: login, список тестов, экран версий/черновика/попытки - ТЗ: V.10 назначения vs активная версия; журнал приёма Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Read-only (по соглашению) пул к hr_bot_test для логина и справки по сотруднику.
|
||||
*/
|
||||
import pg from 'pg';
|
||||
import { getHrPoolConfig } from './poolConfig.js';
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
const cfg = getHrPoolConfig();
|
||||
const pool = cfg ? new Pool(cfg) : null;
|
||||
|
||||
export function getHrPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {unknown[]} [params]
|
||||
*/
|
||||
export async function queryHr(text, params) {
|
||||
if (!pool) {
|
||||
throw new Error('HR database not configured (set HR_DATABASE_URL)');
|
||||
}
|
||||
return pool.query(text, params);
|
||||
}
|
||||
|
||||
export default { getHrPool, queryHr };
|
||||
@@ -0,0 +1,130 @@
|
||||
-- Initial database schema for clinic tests application
|
||||
-- Version: 1.0
|
||||
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Departments table
|
||||
CREATE TABLE IF NOT EXISTS departments (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- User roles enum
|
||||
CREATE TYPE user_role AS ENUM ('hr', 'manager', 'employee');
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
login VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
role user_role NOT NULL DEFAULT 'employee',
|
||||
department_id UUID REFERENCES departments(id),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for login lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_users_login ON users(login);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_department ON users(department_id);
|
||||
|
||||
-- Tests table
|
||||
CREATE TABLE IF NOT EXISTS tests (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
passing_threshold INTEGER DEFAULT 70,
|
||||
time_limit INTEGER,
|
||||
allow_back BOOLEAN DEFAULT true,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_versioned BOOLEAN DEFAULT false,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Test versions table
|
||||
CREATE TABLE IF NOT EXISTS test_versions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_id UUID REFERENCES tests(id) ON DELETE CASCADE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(test_id, version)
|
||||
);
|
||||
|
||||
-- Questions table
|
||||
CREATE TABLE IF NOT EXISTS questions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_version_id UUID REFERENCES test_versions(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
question_order INTEGER NOT NULL,
|
||||
has_multiple_answers BOOLEAN DEFAULT false
|
||||
);
|
||||
|
||||
-- Answer options table
|
||||
CREATE TABLE IF NOT EXISTS answer_options (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
question_id UUID REFERENCES questions(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
is_correct BOOLEAN DEFAULT false,
|
||||
option_order INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Test assignments table
|
||||
CREATE TABLE IF NOT EXISTS test_assignments (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_version_id UUID REFERENCES test_versions(id) ON DELETE CASCADE,
|
||||
assigned_by UUID REFERENCES users(id),
|
||||
deadline DATE,
|
||||
max_attempts INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Assignment targets table
|
||||
CREATE TYPE target_type AS ENUM ('department', 'user');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS test_assignment_targets (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
assignment_id UUID REFERENCES test_assignments(id) ON DELETE CASCADE,
|
||||
target_type target_type NOT NULL,
|
||||
target_id UUID NOT NULL
|
||||
);
|
||||
|
||||
-- Test attempts table
|
||||
CREATE TYPE attempt_status AS ENUM ('in_progress', 'completed', 'expired');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS test_attempts (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_version_id UUID REFERENCES test_versions(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id),
|
||||
attempt_number INTEGER NOT NULL DEFAULT 1,
|
||||
status attempt_status DEFAULT 'in_progress',
|
||||
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
correct_count INTEGER DEFAULT 0,
|
||||
total_questions INTEGER DEFAULT 0,
|
||||
passed BOOLEAN DEFAULT false,
|
||||
UNIQUE(test_version_id, user_id, attempt_number)
|
||||
);
|
||||
|
||||
-- User answers table
|
||||
CREATE TABLE IF NOT EXISTS user_answers (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
attempt_id UUID REFERENCES test_attempts(id) ON DELETE CASCADE,
|
||||
question_id UUID REFERENCES questions(id) ON DELETE CASCADE,
|
||||
selected_options UUID[] DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Settings table
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
-- Insert default admin user (password: admin123)
|
||||
-- This will be done via application code to properly hash the password
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Связь пользователя клиник-теста с сотрудником HR (staff_members.id) — card1 A.x
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS staff_id INTEGER UNIQUE;
|
||||
|
||||
COMMENT ON COLUMN users.staff_id IS 'id из hr_bot_test.staff_members; без дублирования кадров в clinic_tests';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_staff_id ON users(staff_id) WHERE staff_id IS NOT NULL;
|
||||
@@ -40,3 +40,26 @@ export function getPoolConfig(overrides = {}) {
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Пул к БД `hr_bot_test` (сотрудники, users, RBAC) — отдельно от `clinic_tests`.
|
||||
* Только `HR_DATABASE_URL` (без каскадного fallback на `DATABASE_URL` — путаница опасна).
|
||||
* @param {import('pg').PoolConfig} [overrides]
|
||||
* @returns {import('pg').PoolConfig | null}
|
||||
*/
|
||||
export function getHrPoolConfig(overrides = {}) {
|
||||
const url = process.env.HR_DATABASE_URL?.trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
connectionString: url,
|
||||
max: parseInt(process.env.HR_DB_POOL_MAX || '5', 10),
|
||||
idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000', 10),
|
||||
connectionTimeoutMillis: parseInt(
|
||||
process.env.DB_CONNECTION_TIMEOUT || '2000',
|
||||
10
|
||||
),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user