feat: Refactor specialty display logic and enhance specialty filtering

- Introduced SpecialtyChips component to manage the display of doctor's specialties with a primary specialty and a count of additional specialties.
- Updated ItemDoctor component to utilize SpecialtyChips for better UI presentation.
- Enhanced PosterLight and Poster components to display primary specialties and a text line for sub-specialties.
- Implemented nextSpecialtyFilter function to improve specialty selection logic in the Content component.
- Updated search functionality to allow searching by both doctor name and specialty.
- Added new specialties to specialties.json for better coverage.
- Created sync-specialties script to synchronize specialties data with the backend during build.
- Added tests for new components and helper functions to ensure functionality and reliability.
This commit is contained in:
hamed
2026-08-08 17:19:22 +03:30
parent 94c22de8dd
commit 56e264e44c
16 changed files with 799 additions and 52 deletions
+58
View File
@@ -0,0 +1,58 @@
/**
* قاعدهٔ مشترک نمایش تخصص‌های پزشک — منبع واحد کارت و پوستر.
*
* پزشک می‌تواند چند تخصص داشته باشد و معمولاً هم دارد: ذخیرهٔ یک زیرتخصص در
* بک‌اند والدهایش را هم می‌نشاند، پس «جراح گوارش» عملاً یعنی «جراحی عمومی» +
* زیرشاخه. چاپ همهٔ نام‌ها پشت‌سرهم، در موبایل ارتفاع کارت را باد می‌کند و در
* پوستر از کادر ثابت بیرون می‌زند.
*
* نمایش در دو جا متفاوت است — کارت `+N` کلیک‌شدنی دارد و پوستر ندارد — اما
* «کدام تخصص اصلی است» یک قاعده بیشتر نیست و اینجا می‌ماند.
*/
const SEPARATOR = " · ";
/**
* تخصص «اصلی» و بقیه.
*
* ریشه (بدون `parent_id`) اصلی است: عنوانی است که بیمار می‌شناسد و چون هنگام
* ذخیره خودکار اضافه می‌شود تقریباً همیشه وجود دارد. اگر پزشکی فقط زیرتخصص داشت،
* اولین آیتم آرایه.
*
* @param {Array<{name?: string, parent_id?: string|number|null}>} list
* @returns {{primary: object|null, rest: Array<object>}}
*/
export function splitSpecialties(list) {
const items = (list ?? []).filter((s) => s?.name);
if (items.length === 0) return { primary: null, rest: [] };
const primary = items.find((s) => s.parent_id == null) ?? items[0];
return { primary, rest: items.filter((s) => s !== primary) };
}
/**
* زیرتخصص‌ها به‌شکل یک خط متنی، بریده روی مرز کلمه با بودجهٔ کاراکتر.
*
* پوستر کادر ثابت ۱۰۸۰×۱۳۵۰ با `overflow-hidden` دارد؛ هر ردیف اضافه بخش‌های
* پایین را بیرون می‌اندازد. برش بر اساس طول متن است نه تعداد ثابت، چون
* «جراحی لاپاراسکوپی» و «قلب» یک‌اندازه جا نمی‌گیرند.
*
* @param {Array<{name?: string}>} rest
* @param {number} budget حداکثر کاراکترِ خط
* @returns {{text: string, hidden: number}}
*/
export function posterSpecialtyLine(rest, budget = 90) {
const names = (rest ?? []).filter((s) => s?.name).map((s) => s.name);
const shown = [];
let used = 0;
for (const name of names) {
const cost = name.length + (shown.length ? SEPARATOR.length : 0);
if (shown.length && used + cost > budget) break;
shown.push(name);
used += cost;
}
return { text: shown.join(SEPARATOR), hidden: names.length - shown.length };
}