From 45ee92efb02e4481f7a73b830d69071cb1390b0e Mon Sep 17 00:00:00 2001 From: Arshia <20220arshia@gmail.com> Date: Thu, 5 Nov 2026 17:05:56 +0330 Subject: [PATCH] modified data model again. --- docs/analysis/data-model.md | 808 +++++++++++++++++++++++++++++++++++- 1 file changed, 807 insertions(+), 1 deletion(-) diff --git a/docs/analysis/data-model.md b/docs/analysis/data-model.md index 5d537c2..5774249 100644 --- a/docs/analysis/data-model.md +++ b/docs/analysis/data-model.md @@ -14,4 +14,810 @@ **قرارداد نام‌گذاری:** نام جدول‌ها و ستون‌ها به انگلیسی و `snake_case` است (استاندارد فنی پایگاه‌داده). برچسب‌های فارسی در جداول مرجع (`label_fa`) نگهداری می‌شوند تا تغییر متن فارسی نیازی به مهاجرت داده نداشته باشد. این تصمیم با بند «نام بازیگران: فقط نام فارسی» در `CONTRIBUTING.md` §۷ تعارضی ندارد، زیرا آن بند دربارهٔ **متن مستندات** است، نه نام‌گذاری فنی. -## ۱. تصمیم‌های مدل‌سازی \ No newline at end of file +## ۱. تصمیم‌های مدل‌سازی + +| # | تصمیم | گزینهٔ جایگزین | دلیل انتخاب | وابسته به | +|---|---|---|---|---| +| D1 | دانشجو و استاد در یک جدول `users` با ستون `role_code` | دو جدول جدا یا ارث‌بری جدولی | در همهٔ مستندات، دانشجو و استاد رفتار دادهٔ یکسانی دارند و فقط نقششان متفاوت است | — | +| D2 | `location_text` به‌صورت متن آزاد | جدول `locations` با ساختمان/طبقه/اتاق | `US-01.feature` محل را به‌صورت **یک فیلد متنی** با مقدار «ساختمان الف - کلاس ۱۰۲» تعریف کرده است | ابهام دامنه در `README.md` | +| D3 | وضعیت و اولویت و نوع تجهیز و نقش به‌صورت **جدول مرجع** | `ENUM` پایگاه‌داده یا `CHECK` | فهرست وضعیت‌ها و اولویت‌ها در پرسش‌های باز ۳ و ۴ تعریف‌نشده‌اند و باید بدون مهاجرت schema قابل تغییر باشند | پرسش باز ۳ و ۴ | +| D4 | یک گزارش می‌تواند **چند نتیجهٔ تعمیر** داشته باشد | یک نتیجهٔ واحد | `AD-06` مسیر بازگشت پس از رد را نشان می‌دهد؛ اگر این مسیر تأیید شود، چرخهٔ تعمیر تکرار می‌شود | پرسش باز ۷ | +| D5 | تأیید/رد به‌صورت جدول `report_feedbacks` با `UNIQUE (report_id, repair_result_id)` | فقط یک ردیف بازخورد per report | محدودیت یکتا، کلیک دوباره روی «تأیید» را در سطح داده بی‌اثر می‌کند (الزام `US-05.feature`) | — | +| D6 | حذف فیزیکی گزارش ممنوع؛ آرشیو = وضعیت `CLOSED` | ستون `deleted_at` | `NFR-05` از حذف ناخواسته منع می‌کند و بستن گزارش همان نقش آرشیو را دارد | نیاز به حذف منطقی: پرسش باز ۱۲ | +| D7 | `version` برای قفل خوش‌بینانه روی `fault_reports` و `users` | قفل بدبینانه | جلوگیری از بازنویسی هم‌زمان تغییرات (مرتبط با الزام idempotent در `US-05.feature`) | — | +| D8 | `report_no` جدا از `id` | استفاده از `id` به‌عنوان شناسه نمایشی | `US-05.feature` از قالب `R-100` استفاده می‌کند و `US-01.feature` «شناسه یکتا» را نمایش می‌دهد | فرمت تولید: پرسش باز ۱۱ | + +## ۲. فهرست جدول‌ها + +| # | جدول | نوع | توضیح | Aggregate | +|---|---|---|---|---| +| ۱ | `roles` | مرجع | نقش‌های کاربری | — | +| ۲ | `report_statuses` | مرجع | وضعیت‌های گزارش | — | +| ۳ | `priorities` | مرجع | سطوح اولویت | — | +| ۴ | `equipment_types` | مرجع | انواع تجهیز | — | +| ۵ | `notification_channels` | مرجع | کانال‌های اعلان | — | +| ۶ | `users` | اصلی | کاربران سامانه | کاربر | +| ۷ | `fault_reports` | اصلی | گزارش‌های خرابی | گزارش خرابی | +| ۸ | `report_supplementary_info` | اصلی | اطلاعات تکمیلی گزارش | گزارش خرابی | +| ۹ | `report_info_requests` | اصلی | درخواست‌های اطلاعات تکمیلی | گزارش خرابی | +| ۱۰ | `report_assignments` | اصلی | تخصیص تعمیرکار | گزارش خرابی | +| ۱۱ | `repair_results` | اصلی | نتایج تعمیر | گزارش خرابی | +| ۱۲ | `report_feedbacks` | اصلی | تأیید/رد گزارش‌دهنده | گزارش خرابی | +| ۱۳ | `report_history` | اصلی | تاریخچهٔ اقدامات (append-only) | گزارش خرابی | +| ۱۴ | `notifications` | اصلی | اعلان‌ها | اعلان | +| ۱۵ | `report_status_transitions` | اختیاری | انتقال‌های مجاز وضعیت | — | + +> **داشبورد** جدول ندارد؛ یک `VIEW` یا کوئری تجمیعی روی `fault_reports` است (طبق `class-diagram.md` که آن را `Read Model` علامت زده است). + +## ۳. نمودار ERD + +> نمودار به‌صورت انگلیسی نوشته شده تا در همهٔ رندررها (از جمله GitHub) درست نمایش داده شود. توضیحات فارسی در بخش‌های ۴ و ۵ آمده است. + +```mermaid +erDiagram + ROLES ||--o{ USERS : "assigned to" + USERS ||--o{ FAULT_REPORTS : "registers" + REPORT_STATUSES ||--o{ FAULT_REPORTS : "has" + PRIORITIES |o--o{ FAULT_REPORTS : "has" + EQUIPMENT_TYPES ||--o{ FAULT_REPORTS : "classifies" + + FAULT_REPORTS ||--o{ REPORT_SUPPLEMENTARY_INFO : "contains" + USERS ||--o{ REPORT_SUPPLEMENTARY_INFO : "provides" + + FAULT_REPORTS ||--o{ REPORT_INFO_REQUESTS : "contains" + USERS ||--o{ REPORT_INFO_REQUESTS : "requests" + REPORT_SUPPLEMENTARY_INFO |o--o| REPORT_INFO_REQUESTS : "answers" + + FAULT_REPORTS ||--o{ REPORT_ASSIGNMENTS : "contains" + USERS ||--o{ REPORT_ASSIGNMENTS : "is technician" + USERS ||--o{ REPORT_ASSIGNMENTS : "is assigner" + + FAULT_REPORTS ||--o{ REPAIR_RESULTS : "contains" + USERS ||--o{ REPAIR_RESULTS : "records" + + FAULT_REPORTS ||--o{ REPORT_FEEDBACKS : "contains" + REPAIR_RESULTS ||--o{ REPORT_FEEDBACKS : "about" + USERS ||--o{ REPORT_FEEDBACKS : "gives" + + FAULT_REPORTS ||--o{ REPORT_HISTORY : "logs" + USERS ||--o{ REPORT_HISTORY : "acts" + + USERS ||--o{ NOTIFICATIONS : "receives" + FAULT_REPORTS ||--o{ NOTIFICATIONS : "concerns" + NOTIFICATION_CHANNELS ||--o{ NOTIFICATIONS : "via" + + ROLES { + varchar code PK + varchar label_fa + smallint sort_order + boolean is_active + } + REPORT_STATUSES { + varchar code PK + varchar label_fa + smallint sort_order + boolean is_active + } + PRIORITIES { + varchar code PK + varchar label_fa + smallint sort_order + boolean is_active + } + EQUIPMENT_TYPES { + varchar code PK + varchar label_fa + smallint sort_order + boolean is_active + } + NOTIFICATION_CHANNELS { + varchar code PK + varchar label_fa + boolean is_active + } + USERS { + bigserial id PK + varchar username UK + varchar password_hash + varchar role_code FK + varchar display_name + boolean is_active + timestamptz created_at + timestamptz updated_at + integer version + } + FAULT_REPORTS { + bigserial id PK + varchar report_no UK + varchar title + text description + varchar location_text + varchar equipment_type_code FK + varchar status_code FK + varchar priority_code FK + bigint reporter_id FK + timestamptz created_at + timestamptz updated_at + timestamptz closed_at + integer version + } + REPORT_SUPPLEMENTARY_INFO { + bigserial id PK + bigint report_id FK + varchar info_type + text content_text + varchar attachment_ref + bigint provided_by_id FK + timestamptz created_at + } + REPORT_INFO_REQUESTS { + bigserial id PK + bigint report_id FK + bigint requested_by_id FK + text description + timestamptz requested_at + timestamptz responded_at + bigint response_info_id FK + } + REPORT_ASSIGNMENTS { + bigserial id PK + bigint report_id FK + bigint technician_id FK + bigint assigned_by_id FK + timestamptz assigned_at + varchar status + } + REPAIR_RESULTS { + bigserial id PK + bigint report_id FK + bigint technician_id FK + text description + timestamptz recorded_at + } + REPORT_FEEDBACKS { + bigserial id PK + bigint report_id FK + bigint repair_result_id FK + bigint reporter_id FK + boolean is_approved + text rejection_reason + timestamptz created_at + } + REPORT_HISTORY { + bigserial id PK + bigint report_id FK + varchar action_code + bigint actor_id FK + jsonb details + timestamptz occurred_at + } + NOTIFICATIONS { + bigserial id PK + bigint recipient_id FK + bigint report_id FK + varchar event_code + varchar channel_code FK + boolean is_read + timestamptz created_at + timestamptz read_at + } +``` + +## ۴. جداول مرجع (Lookup) + +همهٔ جداول مرجع ساختار یکسانی دارند: `code` (کلید پایدار انگلیسی)، `label_fa` (برچسب فارسی)، `sort_order` و `is_active`. تغییر متن فارسی بدون مهاجرت schema ممکن است و افزودن مقدار جدید نیاز به تغییر کد ندارد. + +### ۴.۱. `roles` — نقش‌ها + +| code | label_fa | sort_order | مرجع | +|---|---|---|---| +| `STUDENT` | دانشجو | ۱۰ | requirements §۲ | +| `PROFESSOR` | استاد | ۲۰ | requirements §۲ | +| `FACILITIES_STAFF` | کارمند تأسیسات | ۳۰ | requirements §۲ | +| `TECHNICIAN` | تعمیرکار | ۴۰ | requirements §۲ | +| `MANAGER` | مدیر | ۵۰ | requirements §۲ | + +> **نکته:** «کاربر سیستم» در `AD-01` و `US-15` یک **نقش** نیست، بلکه عنوان عمومی هر کاربر احراز هویت‌شده است (طبق `activity.md` §۱). بنابراین در این جدول مقدار ندارد. + +### ۴.۲. `report_statuses` — وضعیت‌های گزارش + +| code | label_fa | sort_order | مستند مرجع | +|---|---|---|---| +| `SUBMITTED` | ثبت‌شده | ۱۰ | `US-01.feature`، `US-05.feature` | +| `ASSIGNED` | ارجاع‌شده | ۲۰ | `US-05.feature` | +| `IN_REPAIR` | در حال تعمیر | ۳۰ | `US-05.feature`، `US-12` | +| `REPAIRED` | تعمیر انجام‌شده | ۴۰ | `US-05.feature` | +| `CLOSED` | بسته‌شده | ۵۰ | `US-05.feature` | + +> ⚠️ **پرسش باز ۴:** این فهرست از مثال‌های `US-05.feature` استخراج شده و **رسمی نیست**. «باز» (در `FR-17` و `US-06`) یک وضعیت نیست؛ به‌معنای «هر گزارشی که `status_code <> 'CLOSED'` است» می‌باشد و نباید ردیفی در این جدول داشته باشد. «نیازمند پیگیری» نیز فعلاً برچسب است، نه وضعیت (پرسش باز ۷). + +### ۴.۳. `priorities` — سطوح اولویت + +| code | label_fa | sort_order | مستند مرجع | +|---|---|---|---| +| `LOW` | کم | ۱۰ | **فرض موقت** | +| `MEDIUM` | متوسط | ۲۰ | **فرض موقت** | +| `HIGH` | زیاد | ۳۰ | **فرض موقت** | +| `URGENT` | فوری | ۴۰ | `US-17` (واژهٔ «فوری» آمده) | + +> ⚠️ **پرسش باز ۳:** سطوح رسمی و معیار هر سطح در نیازمندی‌ها تعیین نشده است. سه سطح اول **فرض موقت** هستند و باید پیش از پیاده‌سازی تأیید شوند. + +### ۴.۴. `equipment_types` — انواع تجهیز + +| code | label_fa | مستند مرجع | +|---|---|---| +| `LIGHTING` | روشنایی | `US-01.feature` | +| `HVAC` | تهویه | `README.md` | +| `ELEVATOR` | آسانسور | `README.md` | +| `ELECTRICAL` | برق | `README.md` | +| `PROJECTOR` | پروژکتور | `README.md` | +| `OTHER` | سایر | پیشنهادی (پوشش موارد دیگر) | + +### ۴.۵. `notification_channels` — کانال‌های اعلان + +| code | label_fa | وضعیت | +|---|---|---| +| `IN_APP` | داخل سامانه | **تأییدنشده** | +| `SMS` | پیامک | **تأییدنشده** | +| `EMAIL` | ایمیل | **تأییدنشده** | + +> ⚠️ **پرسش باز ۸:** هیچ کانالی در نیازمندی‌ها تأیید نشده است. این سه مقدار فقط به‌عنوان **دادهٔ نمونه** درج شده‌اند و باید پس از تصمیم‌گیری تیم نهایی شوند. + +## ۵. جداول اصلی (Entities) + +### ۵.۱. `users` — کاربران سامانه + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | شناسه فنی | — | +| `username` | `varchar(50)` | خیر | — | UNIQUE، `CHECK (btrim(username) <> '')` | نام کاربری | `FR-01`، `US-15.feature` | +| `password_hash` | `varchar(255)` | خیر | — | — | هش رمز (هرگز رمز خام) | `FR-01`، `NFR-01` | +| `role_code` | `varchar(30)` | خیر | — | FK → `roles.code` | نقش کاربر | `FR-02` | +| `display_name` | `varchar(100)` | بله | NULL | — | نام نمایشی (در مستندات الزامی نشده) | — | +| `is_active` | `boolean` | خیر | `true` | — | فعال/غیرفعال | — | +| `created_at` | `timestamptz` | خیر | `now()` | — | زمان ایجاد | — | +| `updated_at` | `timestamptz` | خیر | `now()` | — | آخرین تغییر | — | +| `version` | `integer` | خیر | `1` | — | قفل خوش‌بینانه | — | + +**نکته:** `US-15.feature` الزام می‌کند پیام خطای ورود مشخص نکند کدام فیلد اشتباه است. این یک قاعدهٔ **لایهٔ اپلیکیشن** است، نه محدودیت داده. + +> ⚠️ **پرسش باز ۹ (مهم):** اگر تصمیم تیم این باشد که یک کاربر می‌تواند **چند نقش** داشته باشد، باید ستون `role_code` حذف و جدول `user_roles (user_id, role_code)` اضافه شود. مدل فعلی بر پایهٔ **یک نقش per کاربر** است و با `class-diagram.md` (که ارث‌بری دارد) هم‌خوان است، ولی با `domain-model.md` (که نقش را Value Object چندمقداری دیده) هم‌خوان نیست. **پس از پاسخ به پرسش باز ۹، هر سه سند باید هم‌زمان به‌روزرسانی شوند.** + +### ۵.۲. `fault_reports` — گزارش‌های خرابی + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | شناسه فنی | — | +| `report_no` | `varchar(20)` | خیر | — | UNIQUE | شناسهٔ نمایشی (مثل `R-100`) | `US-05.feature` | +| `title` | `varchar(150)` | خیر | — | `CHECK (char_length(title) BETWEEN 1 AND 150)` | عنوان | `US-01.feature`، `FR-03` | +| `description` | `text` | خیر | — | `CHECK (btrim(description) <> '')` | توضیحات | `US-01.feature`، `FR-03` | +| `location_text` | `varchar(255)` | خیر | — | `CHECK (btrim(location_text) <> '')` | محل خرابی (متن آزاد) | `US-01.feature`، `FR-03` | +| `equipment_type_code` | `varchar(30)` | خیر | — | FK → `equipment_types.code` | نوع تجهیز | `US-01.feature`، `FR-03` | +| `status_code` | `varchar(30)` | خیر | `'SUBMITTED'` | FK → `report_statuses.code` | وضعیت جاری | `US-01.feature` | +| `priority_code` | `varchar(30)` | بله | NULL | FK → `priorities.code` | اولویت (تا زمان تعیین خالی است) | `FR-07`، `US-07` | +| `reporter_id` | `bigint` | خیر | — | FK → `users.id` | گزارش‌دهنده | `FR-03`، `US-01`، `US-02` | +| `created_at` | `timestamptz` | خیر | `now()` | — | زمان ثبت | — | +| `updated_at` | `timestamptz` | خیر | `now()` | — | آخرین تغییر | — | +| `closed_at` | `timestamptz` | بله | NULL | — | زمان بستن گزارش | `US-05.feature` | +| `version` | `integer` | خیر | `1` | — | قفل خوش‌بینانه | `US-05.feature` (idempotency) | + +**محدودیت ترکیبی:** `UNIQUE (id, reporter_id)` — برای اینکه جدول بازخورد بتواند تضمین کند فقط گزارش‌دهندهٔ همان گزارش تأیید می‌کند (بخش ۶). + +> **نکتهٔ D2:** `location_text` عمداً متن آزاد است چون `US-01.feature` محل را به‌صورت یک فیلد متنی تعریف کرده است. اگر دامنهٔ سامانه گسترش یابد (ابهام `README.md` دربارهٔ خوابگاه و فضاهای اداری)، در آینده می‌توان `location_text` را نگه داشت و جدول ساختاریافتهٔ `locations` را به‌عنوان مکمل اضافه کرد. `class-diagram.md` فعلاً نسخهٔ ساختاریافته را نشان می‌دهد و این تفاوت باید در بازبینی هم‌راستا شود. + +> **نکتهٔ D6:** حذف فیزیکی ردیف مجاز نیست (`NFR-05`). بستن گزارش با `status_code = 'CLOSED'` و مقدارگرفتن `closed_at` انجام می‌شود. + +### ۵.۳. `report_supplementary_info` — اطلاعات تکمیلی + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK → `fault_reports.id` | گزارش مرتبط | `FR-04`، `US-04` | +| `info_type` | `varchar(20)` | خیر | — | `CHECK (info_type IN ('TEXT','IMAGE'))` | نوع محتوا | `FR-04` | +| `content_text` | `text` | بله | NULL | `CHECK (info_type <> 'TEXT' OR content_text IS NOT NULL)` | متن توضیح | `FR-04` | +| `attachment_ref` | `varchar(500)` | بله | NULL | `CHECK (info_type <> 'IMAGE' OR attachment_ref IS NOT NULL)` | مسیر/شناسهٔ فایل تصویر | `FR-04` | +| `provided_by_id` | `bigint` | خیر | — | FK → `users.id` | ارائه‌دهنده | `US-04` | +| `created_at` | `timestamptz` | خیر | `now()` | — | زمان ارائه | — | + +> **پرسش باز ۶:** `US-04` فقط گزارش‌دهنده را ارائه‌دهنده می‌داند، اما `AD-04` نشان می‌دهد تعمیرکار نیز می‌تواند درخواست اطلاعات دهد. اینکه چه کسی **مجاز به ارائه** است در مستندات صریح نیست؛ مدل فعلی هر کاربری را می‌پذیرد و کنترل آن به لایهٔ اپلیکیشن واگذار شده است. + +### ۵.۴. `report_info_requests` — درخواست اطلاعات تکمیلی + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK → `fault_reports.id` | گزارش مرتبط | `FR-13`، `US-14` | +| `requested_by_id` | `bigint` | خیر | — | FK → `users.id` | درخواست‌دهنده | `US-14`، `AD-04` | +| `description` | `text` | خیر | — | `CHECK (btrim(description) <> '')` | متن درخواست | `US-14` | +| `requested_at` | `timestamptz` | خیر | `now()` | — | زمان درخواست | — | +| `responded_at` | `timestamptz` | بله | NULL | — | زمان پاسخ | `AD-04` | +| `response_info_id` | `bigint` | بله | NULL | FK → `report_supplementary_info.id`، UNIQUE | پاسخ مرتبط | `AD-04` | + +> **پرسش باز ۶:** اینکه چه کسانی مجاز به **درخواست** هستند (فقط تعمیرکار یا کارمند تأسیسات هم) در مستندات مشخص نیست. ستون `requested_by_id` به `users` ارجاع می‌دهد و نقش در `report_history` ثبت می‌شود. + +### ۵.۵. `report_assignments` — تخصیص تعمیرکار (مأموریت) + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK → `fault_reports.id` | گزارش | `FR-09`، `US-08` | +| `technician_id` | `bigint` | خیر | — | FK → `users.id` | تعمیرکار | `US-08`، `US-11` | +| `assigned_by_id` | `bigint` | خیر | — | FK → `users.id` | تخصیص‌دهنده (کارمند تأسیسات) | `AD-03` | +| `assigned_at` | `timestamptz` | خیر | `now()` | — | زمان تخصیص | — | +| `status` | `varchar(20)` | خیر | `'ASSIGNED'` | `CHECK (status IN ('ASSIGNED','STARTED','DONE','CANCELLED'))` | وضعیت تخصیص | `US-12`، `US-13` | + +> ⚠️ **پرسش باز ۵:** آیا یک گزارش می‌تواند هم‌زمان چند تخصیص فعال داشته باشد؟ مدل فعلی این را **مجاز** می‌داند (چند ردیف per report). اگر تصمیم نهایی «یک تعمیرکار در هر لحظه» باشد، باید یک **ایندکس یگانهٔ جزئی** اضافه شود: +> `CREATE UNIQUE INDEX ... ON report_assignments (report_id) WHERE status IN ('ASSIGNED','STARTED');` + +### ۵.۶. `repair_results` — نتایج تعمیر + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK → `fault_reports.id` | گزارش | `FR-12`، `US-13` | +| `technician_id` | `bigint` | خیر | — | FK → `users.id` | ثبت‌کننده | `US-13` | +| `description` | `text` | خیر | — | `CHECK (btrim(description) <> '')` | توضیحات اقدامات | `FR-12` | +| `recorded_at` | `timestamptz` | خیر | `now()` | — | زمان ثبت | — | + +> **نکتهٔ D4:** چند نتیجه per گزارش مجاز است تا اگر مسیر «بازگشت پس از رد» (`AD-06`) تأیید شد، چرخهٔ تعمیر تکرارشدنی باشد (پرسش باز ۷). + +### ۵.۷. `report_feedbacks` — بازخورد گزارش‌دهنده (تأیید/رد) + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK (ترکیبی) | گزارش | `FR-14`، `US-05` | +| `repair_result_id` | `bigint` | خیر | — | FK → `repair_results.id` | نتیجهٔ تعمیر مورد بررسی | `US-05.feature` | +| `reporter_id` | `bigint` | خیر | — | FK (ترکیبی) | تأییدکننده | `US-05.feature` | +| `is_approved` | `boolean` | خیر | — | — | تأیید (`true`) یا رد (`false`) | `US-05.feature` | +| `rejection_reason` | `text` | بله | NULL | `CHECK` (بخش ۶) | دلیل رد | `US-05.feature` | +| `created_at` | `timestamptz` | خیر | `now()` | — | زمان ثبت | — | + +**محدودیت‌های کلیدی:** + +- `UNIQUE (report_id, repair_result_id)` → مانع ثبت دو بازخورد برای یک نتیجهٔ تعمیر؛ همین، کلیک دوباره روی «تأیید» را در سطح داده بی‌اثر می‌کند (`US-05.feature`). +- `FOREIGN KEY (report_id, reporter_id) REFERENCES fault_reports (id, reporter_id)` → **در سطح داده** تضمین می‌کند فقط گزارش‌دهندهٔ همان گزارش می‌تواند بازخورد بدهد (`US-05.feature`). +- `CHECK (is_approved OR (rejection_reason IS NOT NULL AND btrim(rejection_reason) <> ''))` → دلیل رد اجباری (`US-05.feature`). +- `CHECK (NOT is_approved OR rejection_reason IS NULL)` → در حالت تأیید، دلیل رد نباید مقدار داشته باشد. + +> **محدودیتی که در داده قابل اعمال نیست:** «تأیید فقط در وضعیت `REPAIRED` مجاز است» یک قاعدهٔ **بین‌جدولی** است و باید در لایهٔ اپلیکیشن (داخل یک تراکنش با قفل ردیف گزارش) یا با تریگر پیاده شود. در بخش ۶ توضیح داده شده است. + +### ۵.۸. `report_history` — تاریخچهٔ اقدامات + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `report_id` | `bigint` | خیر | — | FK → `fault_reports.id` | گزارش | `FR-16`، `US-10` | +| `action_code` | `varchar(40)` | خیر | — | `CHECK` (بخش ۶) | نوع اقدام | `FR-16`، `US-05.feature` | +| `actor_id` | `bigint` | بله | NULL | FK → `users.id` | انجام‌دهنده (NULL برای اقدام سیستمی) | `FR-16` | +| `details` | `jsonb` | بله | NULL | — | جزئیات (مثلاً دلیل رد) | `US-05.feature` | +| `occurred_at` | `timestamptz` | خیر | `now()` | — | زمان وقوع | `US-10` | + +**مقادیر مجاز `action_code`:** `REPORT_SUBMITTED`، `SUPPLEMENTARY_INFO_ADDED`، `INFO_REQUESTED`، `INFO_PROVIDED`، `PRIORITY_ASSIGNED`، `TECHNICIAN_ASSIGNED`، `REPAIR_STARTED`، `REPAIR_RESULT_RECORDED`، `REPAIR_APPROVED`، `REPAIR_REJECTED`، `REPORT_CLOSED`. + +> **نکته:** این جدول **append-only** است. `UPDATE` و `DELETE` روی آن مجاز نیست و باید با مجوزهای پایگاه‌داده یا تریگر مسدود شود (`NFR-05`). + +### ۵.۹. `notifications` — اعلان‌ها + +| ستون | نوع | Null | پیش‌فرض | محدودیت | توضیح | مرجع | +|---|---|---|---|---|---|---| +| `id` | `bigserial` | خیر | — | PK | — | — | +| `recipient_id` | `bigint` | خیر | — | FK → `users.id` | گیرنده | `FR-15`، `US-16` | +| `report_id` | `bigint` | بله | NULL | FK → `fault_reports.id` | گزارش مرتبط | `US-16` | +| `event_code` | `varchar(40)` | خیر | — | `CHECK` | رویداد مبدأ | `FR-15` | +| `channel_code` | `varchar(20)` | خیر | — | FK → `notification_channels.code` | کانال ارسال | **پرسش باز ۸** | +| `is_read` | `boolean` | خیر | `false` | — | خوانده‌شده | — | +| `created_at` | `timestamptz` | خیر | `now()` | — | زمان ایجاد | — | +| `read_at` | `timestamptz` | بله | NULL | `CHECK ((is_read) = (read_at IS NOT NULL))` | زمان خواندن | — | + +> **مقادیر مجاز `event_code`:** همان مقادیر `action_code` در `report_history` (بخش ۵.۸) به‌اضافهٔ `LOGIN_FAILED` در صورت نیاز. + +## ۶. کلیدها و محدودیت‌ها — خلاصه + +### ۶.۱. کلیدهای اصلی و یکتا + +| جدول | PK | UK | +|---|---|---| +| `roles` | `code` | — | +| `report_statuses` | `code` | — | +| `priorities` | `code` | — | +| `equipment_types` | `code` | — | +| `notification_channels` | `code` | — | +| `users` | `id` | `username` | +| `fault_reports` | `id` | `report_no`، `(id, reporter_id)` | +| `report_supplementary_info` | `id` | — | +| `report_info_requests` | `id` | `response_info_id` | +| `report_assignments` | `id` | — | +| `repair_results` | `id` | — | +| `report_feedbacks` | `id` | `(report_id, repair_result_id)` | +| `report_history` | `id` | — | +| `notifications` | `id` | — | + +### ۶.۲. محدودیت‌های `CHECK` تأییدشده + +| قاعده | جدول | مستند مرجع | +|---|---|---| +| عنوان بین ۱ و ۱۵۰ کاراکتر | `fault_reports` | `US-01.feature` | +| عنوان، توضیحات، محل، نوع تجهیز خالی نباشند | `fault_reports` | `US-01.feature` | +| وضعیت اولیه `SUBMITTED` | `fault_reports` (DEFAULT) | `US-01.feature` | +| دلیل رد هنگام رد اجباری باشد | `report_feedbacks` | `US-05.feature` | +| دلیل رد هنگام تأیید خالی باشد | `report_feedbacks` | `US-05.feature` | +| تأییدکننده همان گزارش‌دهنده باشد | `report_feedbacks` (FK ترکیبی) | `US-05.feature` | +| یک بازخورد per نتیجهٔ تعمیر | `report_feedbacks` (UNIQUE) | `US-05.feature` | +| `read_at` با `is_read` هم‌خوان باشد | `notifications` | — | + +### ۶.۳. قواعدی که در داده قابل اعمال نیستند (لایهٔ اپلیکیشن یا تریگر) + +| قاعده | دلیل | مستند مرجع | +|---|---|---| +| تأیید فقط در وضعیت `REPAIRED` | قاعدهٔ بین‌جدولی | `US-05.feature` | +| تغییر وضعیت فقط از طریق انتقال‌های مجاز | وابسته به پرسش باز ۴ | `activity.md` | +| `closed_at` هنگام `CLOSED` پر باشد | قاعدهٔ بین‌ستونیِ وابسته به جدول مرجع | `US-05.feature` | +| دسترسی مبتنی بر نقش | قاعدهٔ لایهٔ اپلیکیشن | `FR-02`، `US-15.feature` | +| پیام خطای ورود مبهم باشد | قاعدهٔ لایهٔ اپلیکیشن | `US-15.feature` | + +## ۷. ایندکس‌ها + +| جدول | ایندکس | نوع | دلیل | +|---|---|---|---| +| `users` | `(username)` | UNIQUE | ورود سریع، `US-15` | +| `fault_reports` | `(report_no)` | UNIQUE | جست‌وجو با شناسهٔ نمایشی | +| `fault_reports` | `(reporter_id, created_at DESC)` | عادی | «گزارش‌های من»، `FR-05`، `US-03` | +| `fault_reports` | `(status_code, priority_code, created_at DESC)` | عادی | فهرست و فیلتر گزارش‌های باز، `FR-08`، `US-06` | +| `fault_reports` | `(report_id)` روی `(status_code)` WHERE `status_code <> 'CLOSED'` | جزئی | کوئری «گزارش‌های باز» و داشبورد، `FR-17` | +| `report_assignments` | `(technician_id, status)` | عادی | «مأموریت‌های من»، `FR-10`، `US-11` | +| `report_assignments` | `(report_id)` WHERE `status IN ('ASSIGNED','STARTED')` | جزئی (اختیاری) | فقط اگر پاسخ پرسش باز ۵ «یک تعمیرکار هم‌زمان» باشد | +| `report_feedbacks` | `(report_id, repair_result_id)` | UNIQUE | ایدمپوتنسی تأیید، `US-05.feature` | +| `report_history` | `(report_id, occurred_at DESC)` | عادی | نمایش تاریخچه، `FR-16`، `US-10` | +| `notifications` | `(recipient_id, is_read, created_at DESC)` | عادی | فهرست اعلان‌های خوانده‌نشده | +| `report_supplementary_info` | `(report_id, created_at)` | عادی | نمایش اطلاعات تکمیلی | + +## ۸. ماشین حالت در داده + +جدول `report_status_transitions` (اختیاری) ماشین حالت را از کد به **داده** منتقل می‌کند و مستقیماً به پرسش باز ۴ پاسخ می‌دهد: + +| from_status_code | to_status_code | action_code | وضعیت | +|---|---|---|---| +| `SUBMITTED` | `ASSIGNED` | `TECHNICIAN_ASSIGNED` | ✅ تأییدشده (`AD-03`، `FR-09`) | +| `ASSIGNED` | `IN_REPAIR` | `REPAIR_STARTED` | ✅ تأییدشده (`AD-04`، `US-12`) | +| `IN_REPAIR` | `REPAIRED` | `REPAIR_RESULT_RECORDED` | ✅ تأییدشده (`AD-04`، `US-13`) | +| `REPAIRED` | `CLOSED` | `REPAIR_APPROVED` | ✅ تأییدشده (`AD-05`، `US-05.feature`) | +| `REPAIRED` | ؟ | `REPAIR_REJECTED` | ❓ **پرسش باز ۷** | +| `IN_REPAIR` | ؟ (انتظار اطلاعات) | `INFO_REQUESTED` | ❓ **پرسش باز ۶** | + +مزیت: انتقال‌های تأییدشده همین حالا قابل seed هستند و انتقال‌های نامشخص بعداً **فقط با درج ردیف** اضافه می‌شوند، بدون تغییر کد. + +## ۹. یادداشت‌های نرمال‌سازی و نگهداشت + +- **فرم نرمال سوم (3NF):** همهٔ جداول در 3NF هستند؛ نام‌های تکراری (وضعیت، اولویت، نوع تجهیز، نقش، کانال) به جداول مرجع منتقل شده‌اند. +- **عدم تکرار داده:** وضعیت جاری **فقط** در `fault_reports.status_code` نگهداری می‌شود و در `report_history` به‌عنوان رویداد ثبت می‌شود؛ وضعیت در دو جا ذخیره نمی‌شود. +- **`report_history` به‌عنوان audit trail:** این جدول عمداً denormalize نشده و جزئیات در `jsonb` نگهداری می‌شود تا افزودن فیلدهای آینده بدون مهاجرت ممکن باشد. +- **نگهداشت (`NFR-05`):** حذف فیزیکی گزارش و تاریخچه ممنوع است. سیاست دقیق نگهداشت و آرشیو باید تعیین شود (پرسش باز ۱۲). +- **قفل خوش‌بینانه:** `version` روی `fault_reports` و `users` برای جلوگیری از بازنویسی هم‌زمان. هنگام `UPDATE` باید `WHERE version = ?` اعمال و `version = version + 1` شود. +- **منطقهٔ زمانی:** همهٔ زمان‌ها `timestamptz` (با منطقهٔ زمانی) هستند، نه `timestamp` ساده. + +## ۱۰. ردیابی به مستندات + +| عنصر داده | مستند مرجع | +|---|---| +| `users`، `roles` | `FR-01`، `FR-02`، `US-15`، `AD-01` | +| `fault_reports` | `FR-03` تا `FR-05`، `FR-11`، `FR-14` تا `FR-16`، `US-01` تا `US-05`، `US-09`، `US-10`، `AD-02` تا `AD-06` | +| `report_supplementary_info` | `FR-04`، `US-04`، `AD-02`، `AD-04` | +| `report_info_requests` | `FR-13`، `US-14`، `AD-04` | +| `report_assignments` | `FR-09`، `US-08`، `US-11`، `AD-03`، `AD-04` | +| `repair_results` | `FR-12`، `US-13`، `AD-04` | +| `report_feedbacks` | `FR-14`، `US-05`، `AD-05`، `US-05.feature` | +| `report_history` | `FR-16`، `US-03`، `US-05`، `US-10`، `AD-03` | +| `notifications` | `FR-15`، `US-16`، `AD-04` | +| `report_statuses` | `US-01.feature`، `US-05.feature`، `US-12`، `AD-03` تا `AD-05` | +| `priorities` | `FR-07`، `US-07`، `AD-03` (سطوح: پرسش باز ۳) | +| `equipment_types` | `FR-03`، `US-01.feature`، `README.md` | +| محدودیت ۱۵۰ کاراکتر و فیلدهای اجباری | `US-01.feature` | +| محدودیت‌های تأیید/رد | `US-05.feature` | +| یکتایی نام کاربری و هش رمز | `FR-01`، `US-15.feature`، `NFR-01` | + +## ۱۱. پیوست — DDL مرجع (PostgreSQL) + +> ⚠️ این پیوست **غیرالزام‌آور** است و فقط برای نشان‌دادن نگاشت مدل منطقی به یک پیاده‌سازی مشخص آورده شده. اگر تیم DBMS دیگری انتخاب کند (مثلاً MySQL)، این DDL باید معادل‌سازی شود و بخش‌های `jsonb`، `bigserial`، `timestamptz` و ایندکس‌های جزئی نیاز به تطبیق دارند. + +```sql +-- ========================================================= +-- مرجع DDL – PostgreSQL +-- ========================================================= + +-- ۱) جداول مرجع +CREATE TABLE roles ( + code varchar(30) PRIMARY KEY, + label_fa varchar(60) NOT NULL, + sort_order smallint NOT NULL DEFAULT 0, + is_active boolean NOT NULL DEFAULT true +); + +CREATE TABLE report_statuses ( + code varchar(30) PRIMARY KEY, + label_fa varchar(60) NOT NULL, + sort_order smallint NOT NULL DEFAULT 0, + is_active boolean NOT NULL DEFAULT true +); + +CREATE TABLE priorities ( + code varchar(30) PRIMARY KEY, + label_fa varchar(60) NOT NULL, + sort_order smallint NOT NULL DEFAULT 0, + is_active boolean NOT NULL DEFAULT true +); + +CREATE TABLE equipment_types ( + code varchar(30) PRIMARY KEY, + label_fa varchar(60) NOT NULL, + sort_order smallint NOT NULL DEFAULT 0, + is_active boolean NOT NULL DEFAULT true +); + +CREATE TABLE notification_channels ( + code varchar(20) PRIMARY KEY, + label_fa varchar(60) NOT NULL, + is_active boolean NOT NULL DEFAULT true +); + +-- ۲) دادهٔ اولیهٔ جداول مرجع +INSERT INTO roles (code, label_fa, sort_order) VALUES + ('STUDENT', 'دانشجو', 10), + ('PROFESSOR', 'استاد', 20), + ('FACILITIES_STAFF', 'کارمند تأسیسات', 30), + ('TECHNICIAN', 'تعمیرکار', 40), + ('MANAGER', 'مدیر', 50); + +INSERT INTO report_statuses (code, label_fa, sort_order) VALUES + ('SUBMITTED', 'ثبت‌شده', 10), + ('ASSIGNED', 'ارجاع‌شده', 20), + ('IN_REPAIR', 'در حال تعمیر', 30), + ('REPAIRED', 'تعمیر انجام‌شده', 40), + ('CLOSED', 'بسته‌شده', 50); + +-- سطوح اولویت موقت هستند (پرسش باز ۳) +INSERT INTO priorities (code, label_fa, sort_order) VALUES + ('LOW', 'کم', 10), + ('MEDIUM', 'متوسط', 20), + ('HIGH', 'زیاد', 30), + ('URGENT', 'فوری', 40); + +INSERT INTO equipment_types (code, label_fa, sort_order) VALUES + ('LIGHTING', 'روشنایی', 10), + ('HVAC', 'تهویه', 20), + ('ELEVATOR', 'آسانسور', 30), + ('ELECTRICAL', 'برق', 40), + ('PROJECTOR', 'پروژکتور', 50), + ('OTHER', 'سایر', 60); + +-- کانال‌ها تأییدنشده‌اند (پرسش باز ۸) +INSERT INTO notification_channels (code, label_fa) VALUES + ('IN_APP', 'داخل سامانه'), + ('SMS', 'پیامک'), + ('EMAIL', 'ایمیل'); + +-- ۳) کاربران +CREATE TABLE users ( + id bigserial PRIMARY KEY, + username varchar(50) NOT NULL, + password_hash varchar(255) NOT NULL, + role_code varchar(30) NOT NULL REFERENCES roles(code), + display_name varchar(100), + is_active boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + version integer NOT NULL DEFAULT 1, + CONSTRAINT uq_users_username UNIQUE (username), + CONSTRAINT ck_users_username_not_blank CHECK (btrim(username) <> '') +); + +-- ۴) گزارش خرابی +CREATE TABLE fault_reports ( + id bigserial PRIMARY KEY, + report_no varchar(20) NOT NULL, + title varchar(150) NOT NULL, + description text NOT NULL, + location_text varchar(255) NOT NULL, + equipment_type_code varchar(30) NOT NULL REFERENCES equipment_types(code), + status_code varchar(30) NOT NULL DEFAULT 'SUBMITTED' + REFERENCES report_statuses(code), + priority_code varchar(30) REFERENCES priorities(code), + reporter_id bigint NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + closed_at timestamptz, + version integer NOT NULL DEFAULT 1, + CONSTRAINT uq_fault_reports_no UNIQUE (report_no), + CONSTRAINT uq_fault_reports_id_reporter UNIQUE (id, reporter_id), + CONSTRAINT ck_fault_reports_title CHECK (char_length(title) BETWEEN 1 AND 150), + CONSTRAINT ck_fault_reports_description CHECK (btrim(description) <> ''), + CONSTRAINT ck_fault_reports_location CHECK (btrim(location_text) <> '') +); + +-- ۵) اطلاعات تکمیلی +CREATE TABLE report_supplementary_info ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL REFERENCES fault_reports(id), + info_type varchar(20) NOT NULL, + content_text text, + attachment_ref varchar(500), + provided_by_id bigint NOT NULL REFERENCES users(id), + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_supp_info_type CHECK (info_type IN ('TEXT','IMAGE')), + CONSTRAINT ck_supp_info_content CHECK ( + (info_type = 'TEXT' AND content_text IS NOT NULL) OR + (info_type = 'IMAGE' AND attachment_ref IS NOT NULL) + ) +); + +-- ۶) درخواست اطلاعات +CREATE TABLE report_info_requests ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL REFERENCES fault_reports(id), + requested_by_id bigint NOT NULL REFERENCES users(id), + description text NOT NULL, + requested_at timestamptz NOT NULL DEFAULT now(), + responded_at timestamptz, + response_info_id bigint UNIQUE REFERENCES report_supplementary_info(id), + CONSTRAINT ck_info_request_description CHECK (btrim(description) <> '') +); + +-- ۷) تخصیص تعمیرکار +CREATE TABLE report_assignments ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL REFERENCES fault_reports(id), + technician_id bigint NOT NULL REFERENCES users(id), + assigned_by_id bigint NOT NULL REFERENCES users(id), + assigned_at timestamptz NOT NULL DEFAULT now(), + status varchar(20) NOT NULL DEFAULT 'ASSIGNED', + CONSTRAINT ck_assignment_status + CHECK (status IN ('ASSIGNED','STARTED','DONE','CANCELLED')) +); + +-- ۸) نتیجهٔ تعمیر +CREATE TABLE repair_results ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL REFERENCES fault_reports(id), + technician_id bigint NOT NULL REFERENCES users(id), + description text NOT NULL, + recorded_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_repair_result_description CHECK (btrim(description) <> '') +); + +-- ۹) بازخورد گزارش‌دهنده +CREATE TABLE report_feedbacks ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL, + repair_result_id bigint NOT NULL REFERENCES repair_results(id), + reporter_id bigint NOT NULL, + is_approved boolean NOT NULL, + rejection_reason text, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_feedback_per_result UNIQUE (report_id, repair_result_id), + CONSTRAINT fk_feedback_reporter + FOREIGN KEY (report_id, reporter_id) + REFERENCES fault_reports (id, reporter_id), + CONSTRAINT ck_feedback_rejection_reason_required + CHECK (is_approved OR (rejection_reason IS NOT NULL + AND btrim(rejection_reason) <> '')), + CONSTRAINT ck_feedback_rejection_reason_absent + CHECK (NOT is_approved OR rejection_reason IS NULL) +); + +-- ۱۰) تاریخچه (append-only) +CREATE TABLE report_history ( + id bigserial PRIMARY KEY, + report_id bigint NOT NULL REFERENCES fault_reports(id), + action_code varchar(40) NOT NULL, + actor_id bigint REFERENCES users(id), + details jsonb, + occurred_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_history_action CHECK (action_code IN ( + 'REPORT_SUBMITTED','SUPPLEMENTARY_INFO_ADDED','INFO_REQUESTED', + 'INFO_PROVIDED','PRIORITY_ASSIGNED','TECHNICIAN_ASSIGNED', + 'REPAIR_STARTED','REPAIR_RESULT_RECORDED','REPAIR_APPROVED', + 'REPAIR_REJECTED','REPORT_CLOSED' + )) +); + +-- جلوگیری از تغییر تاریخچه (NFR-05) +CREATE RULE report_history_no_update AS + ON UPDATE TO report_history DO INSTEAD NOTHING; +CREATE RULE report_history_no_delete AS + ON DELETE TO report_history DO INSTEAD NOTHING; + +-- ۱۱) اعلان‌ها +CREATE TABLE notifications ( + id bigserial PRIMARY KEY, + recipient_id bigint NOT NULL REFERENCES users(id), + report_id bigint REFERENCES fault_reports(id), + event_code varchar(40) NOT NULL, + channel_code varchar(20) NOT NULL REFERENCES notification_channels(code), + is_read boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + read_at timestamptz, + CONSTRAINT ck_notifications_read_consistency + CHECK (is_read = (read_at IS NOT NULL)) +); + +-- ۱۲) ماشین حالت (اختیاری – پاسخ به پرسش باز ۴) +CREATE TABLE report_status_transitions ( + from_status_code varchar(30) NOT NULL REFERENCES report_statuses(code), + to_status_code varchar(30) NOT NULL REFERENCES report_statuses(code), + action_code varchar(40) NOT NULL, + is_enabled boolean NOT NULL DEFAULT true, + PRIMARY KEY (from_status_code, to_status_code, action_code) +); + +-- فقط انتقال‌های تأییدشده seed می‌شوند؛ +-- انتقال‌های نامشخص (پرسش باز ۶ و ۷) عمداً درج نشده‌اند. +INSERT INTO report_status_transitions + (from_status_code, to_status_code, action_code) VALUES + ('SUBMITTED', 'ASSIGNED', 'TECHNICIAN_ASSIGNED'), + ('ASSIGNED', 'IN_REPAIR', 'REPAIR_STARTED'), + ('IN_REPAIR', 'REPAIRED', 'REPAIR_RESULT_RECORDED'), + ('REPAIRED', 'CLOSED', 'REPAIR_APPROVED'); + +-- ۱۳) ایندکس‌ها +CREATE INDEX ix_fault_reports_reporter + ON fault_reports (reporter_id, created_at DESC); +CREATE INDEX ix_fault_reports_status_priority + ON fault_reports (status_code, priority_code, created_at DESC); +CREATE INDEX ix_fault_reports_open + ON fault_reports (created_at DESC) + WHERE status_code <> 'CLOSED'; +CREATE INDEX ix_assignments_technician + ON report_assignments (technician_id, status); +CREATE INDEX ix_history_report + ON report_history (report_id, occurred_at DESC); +CREATE INDEX ix_notifications_recipient + ON notifications (recipient_id, is_read, created_at DESC); +CREATE INDEX ix_supp_info_report + ON report_supplementary_info (report_id, created_at); + +-- ۱۴) داشبورد – VIEW (نه جدول) +CREATE VIEW v_dashboard_summary AS +SELECT + count(*) FILTER (WHERE status_code <> 'CLOSED') AS open_reports, + count(*) FILTER (WHERE priority_code = 'URGENT' + AND status_code <> 'CLOSED') AS urgent_open_reports, + count(*) AS total_reports +FROM fault_reports; +``` + +## ۱۲. نکات و پرسش‌های باز + +پرسش‌های باز موجود در [`activity.md`](activity.md) که مستقیماً بر مدل داده اثر می‌گذارند، به‌همراه پرسش‌های جدیدی که خود مدل داده ایجاد کرده است: + +### ۱۲.۱. پرسش‌های باز اثرگذار بر مدل داده + +| # | پرسش | اثر بر مدل داده | مرجع | +|---|---|---|---| +| ۳ | سطوح و معیارهای اولویت چیست؟ | مقادیر `priorities` موقت‌اند | `activity.md` | +| ۴ | فهرست رسمی وضعیت‌ها و انتقال‌های مجاز چیست؟ | محتوای `report_statuses` و `report_status_transitions` | `activity.md` | +| ۵ | یک گزارش به یک یا چند تعمیرکار تخصیص می‌یابد؟ | افزودن ایندکس یگانهٔ جزئی در صورت «یک تعمیرکار» | `activity.md` | +| ۶ | چه کسانی مجاز به درخواست/ارائهٔ اطلاعات هستند؟ | کنترل در لایهٔ اپلیکیشن؛ ستون‌ها فعلاً عمومی‌اند | `activity.md` | +| ۷ | پس از رد تعمیر چه اتفاقی می‌افتد؟ | انتقال جدید در `report_status_transitions`؛ شاید ستون `is_flagged_for_followup` | `activity.md` | +| ۸ | کانال اعلان چیست؟ | مقادیر `notification_channels` تأییدنشده | `activity.md` | +| ۹ | آیا یک کاربر می‌تواند چند نقش داشته باشد؟ | **تعیین‌کننده:** ستون `role_code` یا جدول `user_roles` | `activity.md` | +| ۱۰ | روش احراز هویت و بازیابی رمز چیست؟ | ممکن است جدول `password_reset_tokens` و `login_attempts` لازم شود | `activity.md` | + +### ۱۲.۲. پرسش‌های باز جدید (ایجادشده توسط مدل داده) + +| # | پرسش | پیشنهاد پیش‌فرض | +|---|---|---| +| ۱۱ | فرمت و روش تولید `report_no` چیست؟ (توالی، تصادفی، شامل سال؟) | `R-` + توالی یکتا؛ الگوی دقیق باید تعیین شود | +| ۱۲ | آیا حذف منطقی گزارش لازم است یا بستن (`CLOSED`) کافی است؟ | فعلاً بستن کافی است (`NFR-05`) | +| ۱۳ | آیا ثبت تلاش‌های ورود ناموفق برای audit لازم است؟ | در مستندات الزام نشده؛ در صورت نیاز جدول `login_attempts` اضافه شود | +| ۱۴ | آیا محل خرابی باید ساختاریافته شود (ساختمان/طبقه/اتاق)؟ | تا تعیین ابهام دامنه، `location_text` کافی است (تصمیم D2) | +| ۱۵ | سیاست نگهداشت و آرشیو گزارش‌های بسته‌شده چیست؟ | باید در `NFR-05` یا سند جداگانه تعیین شود | + +### ۱۲.۳. ناسازگاری‌هایی که باید در بازبینی حل شوند + +1. **`location_text` در برابر `Location` ساختاریافته:** `class-diagram.md` و `domain-model.md` محل را ساختاریافته مدل کرده‌اند، اما `US-01.feature` آن را یک فیلد متنی می‌داند. تصمیم D2 این سند، متن آزاد است. باید یکی از دو سند همراستا شود. +2. **ارث‌بری نقش در برابر ستون `role_code`:** `class-diagram.md` از ارث‌بری استفاده می‌کند، `domain-model.md` نقش را Value Object چندمقداری دیده و این سند یک ستون واحد دارد. پاسخ پرسش باز ۹ باید **هر سه سند را هم‌زمان** به‌روزرسانی کند. +3. **`Dashboard`:** در `class-diagram.md` یک کلاس است، در این سند یک `VIEW`. این تفاوت عمدی است و باید در `class-diagram.md` با برچسب `Read Model` شفاف‌تر شود. + +--- + +**وضعیت سند:** نسخه اولیه (Draft) – آماده بازبینی \ No newline at end of file